/// This is a simple implementation of the 2048 game /// It's playable in fsi, I might hook it up to an app later using Xamarin's tooling. /// But so far it was just funny to implement the rules. namespace T2048 module Game = type Row = List type Board = List type Direction = LEFT | RIGHT | UP | DOWN let NEW_TILES = [2; 4] let mutable theScore = 0 let addScore s = theScore <- theScore + s // Here is the main logic of the game, moving the tiles and combining identical neighbours // We always move from right to left, other directions are performed by turning the board first let rec moveRow r = match r with | [] -> [] | 0::bs -> moveRow bs | [a] -> [a] | a::b::bs when b=0 -> moveRow(a::bs) | a::b::bs when a=b -> addScore(a+b) ; (a+b)::moveRow bs | a::b::bs -> a::moveRow(b::bs) let pad elem toLen bs = let padLen = toLen - List.length bs if padLen<=0 then bs else let newTail = [for i in 1 .. padLen -> elem] bs @ newTail let moveBoard b = let size = List.length b let moveAndPad = moveRow >> (pad 0 size) List.map moveAndPad b let reverseBoard = List.map List.rev let rec transposeBoard b = match b with | [] -> [] | []::_ -> [] | _ -> let heads = List.map List.head b let tails = List.map List.tail b heads :: transposeBoard tails // Turn the board, move left, and turn back let moveDirection d = match d with | LEFT -> moveBoard | RIGHT -> reverseBoard >> moveBoard >> reverseBoard | UP -> transposeBoard >> moveBoard >> transposeBoard | DOWN -> transposeBoard >> reverseBoard >> moveBoard >> transposeBoard >> List.rev let emptySlots b = let slot i j v = ((i,j),v) let numberRow f row = List.mapi f row let numberBoard = List.mapi (fun i -> numberRow (slot i)) let empties = List.choose (fun (a,b) -> if b=0 then Some(a) else None) b |> numberBoard |> List.concat |> empties let rnd = new System.Random() let oneOf vs = let i = rnd.Next(List.length vs) List.nth vs i let updateBoard coords v board = let (posRow,posCol) = coords let updateRow pos v row = List.mapi (fun i b -> if i=pos then v else b) row List.mapi (fun i row -> if i=posRow then updateRow posCol v row else row) board let addNewTile b = let empties = emptySlots b let slotCoords = oneOf empties let v = oneOf NEW_TILES updateBoard slotCoords v b // To detect "game over", try a direction which is 90 degress from the current one and see if any tiles can move let otherDirection d = match d with | LEFT -> UP | RIGHT -> UP | UP -> LEFT | DOWN -> LEFT // Perform a move let nextMove b d = let newBoard = b |> (moveDirection d) |> addNewTile let gameOver = newBoard=b && (moveDirection (otherDirection d) b)=b (newBoard, gameOver) // Install a pretty printer if we are running in fsi let buildString sep l = List.foldBack (fun a acc -> sprintf "%4i %s %s" a sep acc) l "" let printBoard (b:Board) = b |> List.map (buildString "|") |> List.reduce (fun a b -> a+"\n"+b) #if INTERACTIVE fsi.AddPrinter printBoard #endif // Here is the current state let mutable theBoard:Board = [] // Start a new game let reset () = let _ = theScore <- 0 let _ = theBoard <- [[0;0;0;0]; [0;0;0;0]; [0;0;0;0]; [0;0;0;0]] |> addNewTile theBoard // Make a move and update the current state let move d = let (newBoard, gameOver) = nextMove theBoard d let _ = if gameOver then printf "Game over, score=%i" theScore let _ = theBoard <- newBoard theBoard