Skip to content

# mthvedt/battleship

Some cleanup

• Loading branch information...
1 parent c47fb2b commit 3f6f5563aec0357ddb435f72755ef0c7e06741af committed
Showing with 90 additions and 68 deletions.
1. +12 −17 README.md
2. battleship.jar
3. +20 −21 src/battleship/ai.clj
4. +56 −29 src/battleship/core.clj
5. +2 −1 src/battleship/game.clj
29 README.md
 @@ -4,14 +4,16 @@ A Monte Carlo Battleship AI, in Clojure. ## Motivation -I wrote this as a coding exercise to get the hang of functional programming techniques and probabilistic methods. Also, because I enjoy wasting my time playing games. +A coding exercise/code sample to work with functional programming techniques of calculation. In particular, probabilistic Monte Carlo methods. ## Description -Battleship is a Clojure (JVM, Java-compatible, highly functional LISP) command-line app that allows you to play a game of the classic board game, Battleship, against the computer. The computer uses a Monte Carlo solver to guess where your ships are. +Battleship is a Java command line app that allows you to play a game of the classic board game, Battleship, against the computer. The computer uses a Monte Carlo solver to guess where your ships are. Currently, ship placement is random. You can't place your own ships. Maybe next release. +Battleship is written in Clojure, a LISP-like functional programming language that compiles down to Java bytecode. It will run anywhere Java can run. + ## How to play Download the distribution, or the standalone jar, and run: @@ -23,27 +25,20 @@ and follow the instructions. Enjoy! ### The solver -The computer generates an infinite sequence of possible boards, i.e., arrangements of ships the player might have, The sequence is lazy, boards not being generated until they are used. -It then filters this sequence, rejecting boards that it knows to be impossible--for instance, if a ship is someplace the computer fired upon and missed, or if the computer hit a ship and there's no ship there, that board is rejected as invalid. This generates another infinite sequence. -The computer then takes some fixed number of valid boards--100 in this release--and uses these to calculate where the player's ships most likely are. -It uses a greedy firing algorithm, firing on the square most likely to contain a ship. The computer does not plan ahead. Even with this greedy algorithm, it is still pretty good. - -### Implementation - -The first implementation used naive generation--any possible board was generated, and then rejected. As the board filled up with hits and misses, this became unacceptably slow. So the rejection criteria were divided into two: one set taken into account at board generation time, to efficiently generate boards that are more likely to be valid. The second set operates after board generation time, rejecting boards that turned out invalid anyway. This implementation is much faster; however, it still slows down in some edge cases--in particular, if the computer has struck several ships, but not sunk them yet. - -Some impossible positions are not actually rejected. For instance, suppose the computer has fired at, and hit C5, D5, E5 and F5, without having sunk a ship. The computer may generate some speculative boards where these four squares are occupied by a battleship (which is four spaces long); even though that's impossible because it would have been sunk. These impossible positions seem to have a negligible effect on the computer's play, and so are not rejected because the programmer is a little lazy. +The AI uses a greedy firing algorithm. Given some known facts about the players pieces are--which squares have hits, which squares have misses, which squares have sunken ships, and which ships are sunk--the AI attempts to guess where the player's ships are most likely to be, and then fires upon the square most likely to contain a ship. +To do this, the computer generates a sequence of random ship positions (boards) the player might have. It then reduces some number of positions into a random probability distribution. +Clojure was chosen primarily because the author wanted to learn Clojure. This problem was chosen because the author wanted to play with practical Monte Carlo probability simulations, and Clojure is near ideal for quick algorithmic coding and experimentation. The implementation is concise, though not as concise as a master of this language might come up with. The entire AI module, for example, is about 90 lines of code, including comments. As Clojure is a functional language that encourages modular composition of higher-order functions, instead of control structures and imperative steps, the elements of an algorithmic idea can quickly be coded, mixed, matched, debugged, and rewritten simply by writing and rearranging functions. ### Thoughts -* The random distribution is as follows: it places one ship randomly, then another, then another, until it's done. This looks uniformly random, but it actually isn't. For instance, positions where there are more options to place the last ship will be less likely than positions where there are fewer ways to place the last ship. Nevertheless, it is "random enough" for now. -* The computer doesn't make any short-circuit inferences. For instance, when it might be totally clear to a human player where his last carrier is located, the computer will still run a full simulation to find it. This can sometimes be noticeably slow. -* On that note, there is certainly a lot of room for performance improvements. -* The computer always starts off by firing at the center, which is where pieces are most likely to be placed in its random simulations. A human, given the option, can exploit this by preferring edge and corner positions. +* The computer generates random positions as follows: it places one ship randomly, then another, then another, until it's done. Any one ship misplacement--for instance, a ship being placed in a 'miss' square, or two squares overlapping--causes the entire position to be rejected. This prevents sample space bias--if you imagine piece placements as a probability tree, with each branch being a piece placement and the leaves being fully placed boards, it's important not to misweight the leaves by equal weighting the branch subdivisions further up when one branch has more leaves, yielding nonuniform probability weights to the leaves. +* The computer doesn't make any short-circuit inferences. For instance, when it might be totally clear to a human player where his last carrier is located, the computer will still run a full simulation to find it. This can be slow in edge cases. +* The code is not heavily optimized, so there is room for other performance improvements as well. +* The computer always starts off by firing at the center, which is where pieces are most likely to be placed in its random simulations. A human, given the option, can exploit this by preferring edge and corner positions. Fortunately for the computer, the human is not given the option in this version! ## License -This work is public domain. This work comes distributed with Clojure, which is released under the Eclipse Public License. +This work is public domain. This work's binary comes distributed with Clojure, which is released under the Eclipse Public License. ## And, finally...
BIN battleship.jar
Binary file not shown.
41 src/battleship/ai.clj
 @@ -3,17 +3,17 @@ ; A Monte Carlo based AI for Battleship. -; Given a square on a board, tells what the AI -; is "allowed to know" about it. A square may have one of the given pieces, -; not have a ship (blocked), or be unknown. +; Given a square on a board, tells what the AI knows about it. +; It may know there's a live ship there, no ship there, +; or the state may be wholly unknown. (defn get-knowledge [square mypieces] (if (= :struck (:state square)) (if (nil? (get mypieces (:piece square))) :blocked ; There's a sunk ship or no ship here :has-ship) ; We know there's an unsunk ship here - :unknown)) ; We don't know what's here + :unknown)) ; We don't know what's here (haven't shot here yet) -; [x, y] running thru [10, 10]. +; [x, y] running thru [10, 10] (def all-coordinates (for [x (range 10) y (range 10)] [x y])) ; A map [x, y] -> what is known about it @@ -31,17 +31,13 @@ ; Makes sure that, for some board, all squares known to have a ship ; do in fact have a ship. (defn struck-square-checker [candidate-board kmap] - (let [rval (loop [coordinate (first all-coordinates) - coordinates (rest all-coordinates)] - (if (nil? coordinate) - true ; loop over - (let [[x y] coordinate] - (if (= :has-ship (get kmap coordinate)) - (if (nil? (:piece (get-square candidate-board x y))) - false ; square should have a ship, but it didn't - (recur (first coordinates) (rest coordinates))) - (recur (first coordinates) (rest coordinates))))))] - rval)) + (empty? (filter (fn [[x y]] + (and (= :has-ship (get kmap [x y])) + (nil? (:piece (get-square candidate-board x y))))) + ; The filter will find a square that is 'known' + ; to have a ship but doesn't have one. Any such square + ; causes the board to be rejected. + all-coordinates))) ; Given a known-board, containing struck and unstruck squares, ; and pieces, containing unsunk ships; @@ -49,9 +45,10 @@ ; that match these criteria. (defn infinite-boards [known-board mypieces] (let [kmap (knowledge-map known-board mypieces)] - (filter #(struck-square-checker % kmap) - (repeatedly #(place-all-pieces newboard mypieces - (blocked-square-validator kmap)))))) + (filter + #(and (not (nil? %)) (struck-square-checker % kmap)) + (repeatedly #(try-place-all-pieces newboard mypieces + (blocked-square-validator kmap)))))) ; 1 if we might want to shoot that square, 0 otherwise (defn is-target [square] @@ -64,7 +61,8 @@ ; ; Doalls are used here to prevent lazy reduction. ; In some cases, reducing a lazy seq with a lazy fn -; can produce a tower of calls. Most LISP-like languages take care of this +; can produce a large tower of nested fn calls and cause a stack overflow. +; Most LISP-like languages take care of this ; with tail-call optimization; the JVM can't. (defn get-distribution [boardseq] (reduce (fn [running-count board] @@ -81,7 +79,8 @@ (map #(vector % %2 y) row (range))) dist (range)) filtered-cvt (filter (fn [[_ x y]] ; remove all struck squares - (= :unstruck (:state (get-square theboard x y)))) + (= :unstruck + (:state (get-square theboard x y)))) coordinate-value-tuples)] (rest (apply max-key first filtered-cvt)))) ; return (x, y)
85 src/battleship/core.clj
 @@ -1,11 +1,15 @@ (ns battleship.core) ; Very basic battleship stuff goes here. +; a utility fn--repeatedly tries a form until it yields not nil +(defmacro retrying [& forms] + `(first (remove nil? (repeatedly (fn [] ~@forms))))) + ; A square can be empty or contain a ship. ; A square can be in three states: unstruck, struck, or sunk. (defrecord Square [piece state]) -; Boards and squares. +; Boards and squares. A board is a 2-d sequence of sequences of squares. (def board-size 10) (def newboard (vec (repeat board-size (vec (repeat board-size (Square. nil :unstruck)))))) @@ -17,6 +21,15 @@ (let [row (nth board y)] (assoc board y (assoc row x square)))) +; the validator function should take in the original board, x, and y +; and return true if the validator will allow a piece to place there +(defn set-valid-square [board x y square validator] + (if (nil? board) + nil + (if (validator board x y) + (set-square board x y square) + nil))) + ; The pieces in the canonical US version of Battleship. (def pieces [["carrier" 5] @@ -25,53 +38,67 @@ ["submarine" 3] ["destroyer" 2]]) -; A hash map version. +; A hash map of the above. (def pieces-map (reduce conj {} pieces)) +; helper fn for place-piece +(defn get-range [coord0 step?] + (if step? + (range coord0 Double/POSITIVE_INFINITY) + (repeat coord0))) + ; places a piece on the board, or nil if it can't be placed according ; to the given validator fn -; the validator function should take in the original board, x, and y -; and return true if the validator will allow a piece to place there -; -; this allows us to generate random boards (with randomly-try-place-piece -; below) according to certain constraints. (defn place-piece [board0 [piecename piecelen] x0 y0 is-horizontal validator] - (let [xstep (if is-horizontal 1 0) - ystep (if is-horizontal 0 1)] - (loop [board board0 x x0 y y0 i 0] - (if (= i piecelen) - board - (if (validator board0 x y) - (recur (set-square board x y (Square. piecename :unstruck)) - (+ x xstep) (+ y ystep) (inc i)) - nil))))) - -; try once to place a piece, return nil if failed -(defn randomly-try-place-piece [board [piecename piecelen] validator] + (let [xrange (get-range x0 is-horizontal) + yrange (get-range y0 (not is-horizontal)) + placer (fn [board [x y]] (set-valid-square board + x y + (Square. piecename + :unstruck) + validator))] + (reduce placer board0 (take piecelen (map vector xrange yrange))))) + +(defn occupied-validator [board x y] + (nil? (get (get-square board x y) :piece))) + +; return a random [x, y] coordinate, or nil +(defn try-random-piece-coords [board piecelen validator] (let [is-horizontal (= (rand-int 2) 0) coord-a (rand-int board-size) - ; subtract piecelen; make sure the piece doesn't overflow off the board + ; make sure the piece doesn't overflow off the board coord-b (rand-int (- board-size piecelen))] (let [x (if is-horizontal coord-b coord-a) y (if is-horizontal coord-a coord-b)] - (place-piece board [piecename piecelen] x y is-horizontal validator)))) + (if (place-piece board ["dummy" piecelen] x y is-horizontal validator) + [x y is-horizontal] + nil)))) ; try (possibly forever!) to place a piece ; works by making an infinite sequence of randomly-try-place-piece calls -; and pulling the first one -(defn randomly-place-piece [board piece validator] - (first (remove nil? (repeatedly - #(randomly-try-place-piece board piece validator))))) +; and pulling the first one that passes the validator. +; +; boards with overlapping pieces are destroyed outright (returning nil), +; instead of trying again. this prevents "restricted choice" +; or "monty hall" biasing of the solution space. the full math behind it +; is too much to fit here +(defn try-randomly-place-piece [board [piecename piecelen :as piece] + validator] + (if (nil? board) + nil + (let [[x y is-horizontal] (retrying (try-random-piece-coords + board piecelen validator))] + (place-piece board piece x y is-horizontal occupied-validator)))) -(defn place-all-pieces +; Can return nil. +(defn try-place-all-pieces ; Places all 5 default pieces on the given board - ([board] (place-all-pieces board pieces - #(nil? (:piece (get-square % %2 %3))))) + ([board] (try-place-all-pieces board pieces (fn [& _] true))) ; Places all the given pieces on the given board according ; to a given validator. ([board mypieces validator] (reduce - #(randomly-place-piece % %2 validator) + #(try-randomly-place-piece % %2 validator) board mypieces))) ; Below are some methods for printing to console.
3 src/battleship/game.clj
 @@ -7,7 +7,8 @@ ; a board together with some info about the game state (defn new-decorated-board [] - (DecoratedBoard. (place-all-pieces newboard) pieces-map nil)) + (DecoratedBoard. (retrying (try-place-all-pieces newboard)) + pieces-map nil)) ; canonically, board1 is player's board (he fires upon board2) (defrecord Game [board1 board2])

#### 0 comments on commit `3f6f556`

Please sign in to comment.
Something went wrong with that request. Please try again.