diff --git a/AsciiLock.hs b/AsciiLock.hs
new file mode 100644
--- /dev/null
+++ b/AsciiLock.hs
@@ -0,0 +1,245 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module AsciiLock (lockToAscii, lockOfAscii, stateToAscii
+    , readAsciiLockFile, writeAsciiLockFile) where
+
+import Data.Function (on)
+import Control.Applicative hiding ((<*>))
+import Control.Monad
+import Control.Arrow ((&&&))
+import System.IO
+import qualified Data.Map as Map
+import Data.Map (Map)
+import qualified Data.Vector as Vector
+import Data.Vector (Vector, (!), (//))
+import Data.Maybe
+import Data.Traversable as T
+import Data.List
+
+import Lock
+import GameStateTypes
+import GameState
+import BoardColouring
+import Frame
+import Hex
+import CVec
+import Util
+import Mundanities
+
+
+type AsciiLock = [String]
+
+lockToAscii :: Lock -> AsciiLock
+lockToAscii (_,st) = stateToAscii st
+
+stateToAscii :: GameState -> AsciiLock
+stateToAscii st =
+    let colouring = boardColouring st (ppidxs st) Map.empty
+    in boardToAscii colouring . stateBoard $ st
+
+lockOfAscii :: AsciiLock -> Maybe Lock
+lockOfAscii lines = do
+    board <- asciiToBoard lines
+    let size = maximum $ map (hx . (<->origin))$ Map.keys board
+	frame = BasicFrame size
+    st <- asciiBoardState frame board
+    return (frame, st)
+
+boardToAscii :: PieceColouring -> GameBoard -> AsciiLock
+boardToAscii colouring board =
+    let asciiBoard :: Map CVec Char
+	asciiBoard = Map.mapKeys (hexVec2CVec . (<->origin))
+	    $ fmap (monochromeOTileChar colouring) board
+	(miny,maxy) = minmax $ map cy $ Map.keys asciiBoard
+	(minx,maxx) = minmax $ map cx $ Map.keys asciiBoard
+	asciiBoard' = Map.mapKeys (<->CVec miny minx) asciiBoard
+    in [ [ Map.findWithDefault ' ' (CVec y x) asciiBoard' 
+	    | x <- [0..(maxx-minx)] ]
+	| y <- [0..(maxy-miny)] ]
+
+asciiToBoard :: AsciiLock -> Maybe GameBoard
+asciiToBoard lines =
+    let asciiBoard :: Map CVec Char
+	asciiBoard = Map.fromList [(CVec y x,ch)
+	    | (line,y) <- zip lines [0..]
+	    , (ch,x) <- zip line [0..]
+	    , ch `notElem` "\t\r\n "]
+	(miny,maxy) = minmax $ map cy $ Map.keys asciiBoard
+	midy = miny+(maxy-miny)`div`2
+	midline = filter ((==midy).cy) $ Map.keys asciiBoard
+	(minx,maxx) = minmax $ map cx $ midline
+	centre = CVec midy (minx+(maxx-minx)`div`2)
+    in Map.mapKeys ((<+>origin) . cVec2HexVec . (<->centre))
+	<$> T.mapM monoToOTile asciiBoard
+
+asciiBoardState :: Frame -> GameBoard -> Maybe GameState
+asciiBoardState frame board =
+    let addPreBase st = foldr addpp st (replicate 6 $ PlacedPiece origin $ Block [])
+	addBase st = foldr addBaseOT st $ Map.toList $
+	    Map.filter (isBaseTile.snd) board
+	isBaseTile (BlockTile _) = True
+	isBaseTile (PivotTile _) = True
+	isBaseTile HookTile = True
+	isBaseTile (WrenchTile _) = True
+	isBaseTile (BallTile) = True
+	isBaseTile _ = False
+	addBaseOT :: (HexPos,(PieceIdx,Tile)) -> GameState -> GameState
+	addBaseOT (pos,(o,BlockTile [])) = addBlockPos o pos
+	addBaseOT (pos,(-1,t)) = addpp $ PlacedPiece pos $ basePieceOfTile t
+	basePieceOfTile (PivotTile _) = Pivot []
+	basePieceOfTile HookTile = Hook hu NullHF
+	basePieceOfTile (WrenchTile _) = Wrench zero
+	basePieceOfTile BallTile = Ball
+	componentifyNew st = foldr ((fst.).componentify) st $ filter (/=0) $ ppidxs st
+	-- | we assume that the largest wholly out-of-bounds block is the frame
+	setFrame st = fromMaybe st $ do
+	    (idx,pp) <- listToMaybe $ sortBy (flip compare `on` blockSize . placedPiece . snd)
+		    [ (idx,pp)
+		    | (idx,pp) <- enumVec $ placedPieces st
+		    , isBlock . placedPiece $ pp
+		    , let fp = plPieceFootprint pp
+		    , not $ null fp
+		    , all (not.inBounds frame) fp
+		    ]
+	    return $ delPiece idx $ setpp 0 pp st
+	blockSize (Block vs) = length vs
+	baseSt = setFrame . componentifyNew . addBase . addPreBase $ GameState Vector.empty []
+
+	baseBoard = stateBoard baseSt
+	addAppendages :: GameState -> Maybe GameState
+	addAppendages st = foldM addAppendageOT st $ Map.toList $
+	    Map.filter (not.isBaseTile.snd) board
+	addAppendageOT st (pos,(-1,ArmTile dir _)) = 
+	    let rpos = (neg dir<+>pos)
+	    in case Map.lookup rpos baseBoard of
+		Just (idx,PivotTile _) -> Just $ addPivotArm idx pos st
+		Just (idx,HookTile) -> Just $ setpp idx (PlacedPiece rpos (Hook dir NullHF)) st
+		_ -> Nothing
+	addAppendageOT st (pos,(-1,SpringTile extn dir)) =
+	    let rpos = (neg dir<+>pos)
+	    in case Map.lookup rpos baseBoard of
+		Just (_,SpringTile _ _) -> Just st
+		Just (ridx,tile) -> do
+		    (_,epos) <- castRay pos dir baseBoard
+		    let twiceNatLen = sum [ extnValue extn
+			    | i <- [1..hexLen (epos<->rpos)-1]
+			    , let pos' = i<*>dir<+>rpos
+			    , Just (_,SpringTile extn _) <- [ Map.lookup pos' board ] ]
+			extnValue Compressed = 4
+			extnValue Relaxed = 2
+			extnValue Stretched = 1
+			Just root = posLocus baseSt rpos
+			Just end = posLocus baseSt epos
+		    Just $ flip addConn st $ Connection root end $ Spring dir $ twiceNatLen`div`2
+		_ -> Just st
+	addAppendageOT _ _ = Nothing
+    in addAppendages baseSt
+
+monochromeOTileChar :: PieceColouring -> OwnedTile -> Char
+monochromeOTileChar colouring (idx,BlockTile _) =
+    case Map.lookup idx colouring of
+	Just 1 -> '%'
+	Just 2 -> '"'
+	Just 3 -> '&'
+	Just 4 -> '~'
+	_ -> '#'
+monochromeOTileChar _ (_,t) = monochromeTileChar t
+monochromeTileChar (PivotTile _) = 'o'
+monochromeTileChar (ArmTile dir _) 
+    | dir == hu = '-'
+    | dir == hv = '\\'
+    | dir == hw = '/'
+    | dir == neg hu = '.'
+    | dir == neg hv = '`'
+    | dir == neg hw = '\''
+monochromeTileChar HookTile = '@'
+monochromeTileChar (WrenchTile _) = '*'
+monochromeTileChar BallTile = 'O'
+monochromeTileChar (SpringTile extn dir)
+    | dir == hu = case extn of
+	Stretched                  -> 's'
+	Relaxed                    -> 'S'
+	Compressed                 -> '$'
+    | dir == hv = case extn of
+	Stretched                  -> 'z'
+	Relaxed                    -> 'Z'
+	Compressed                 -> '5'
+    | dir == hw = case extn of
+	Stretched                  -> '('
+	Relaxed                    -> '['
+	Compressed                 -> '{'
+    | dir == neg hu = case extn of
+	Stretched                  -> 'c'
+	Relaxed                    -> 'C'
+	Compressed                 -> 'D'
+    | dir == neg hv = case extn of
+	Stretched                  -> ')'
+	Relaxed                    -> ']'
+	Compressed                 -> '}'
+    | dir == neg hw = case extn of
+	Stretched                  -> '1'
+	Relaxed                    -> '7'
+	Compressed                 -> '9'
+monoToOTile :: Char -> Maybe OwnedTile
+monoToOTile '#' = Just $ (1,BlockTile [])
+monoToOTile '%' = Just $ (2,BlockTile [])
+monoToOTile '"' = Just $ (3,BlockTile [])
+monoToOTile '&' = Just $ (4,BlockTile [])
+monoToOTile '~' = Just $ (5,BlockTile [])
+monoToOTile ch = ((,) (-1)) <$> monoToTile ch
+monoToTile 'o' = Just $ PivotTile zero
+monoToTile '-' = Just $ ArmTile hu False
+monoToTile '\\' = Just $ ArmTile hv False
+monoToTile '/' = Just $ ArmTile hw False
+monoToTile '.' = Just $ ArmTile (neg hu) False
+monoToTile '`' = Just $ ArmTile (neg hv) False
+monoToTile '\'' = Just $ ArmTile (neg hw) False
+monoToTile '@' = Just $ HookTile
+monoToTile '*' = Just $ WrenchTile zero
+monoToTile 'O' = Just $ BallTile
+monoToTile 's' = Just $ SpringTile Stretched hu
+monoToTile 'S' = Just $ SpringTile Relaxed hu
+monoToTile '$' = Just $ SpringTile Compressed hu
+monoToTile 'z' = Just $ SpringTile Stretched hv
+monoToTile 'Z' = Just $ SpringTile Relaxed hv
+monoToTile '5' = Just $ SpringTile Compressed hv
+monoToTile '(' = Just $ SpringTile Stretched hw
+monoToTile '[' = Just $ SpringTile Relaxed hw
+monoToTile '{' = Just $ SpringTile Compressed hw
+monoToTile 'c' = Just $ SpringTile Stretched (neg hu)
+monoToTile 'C' = Just $ SpringTile Relaxed (neg hu)
+monoToTile 'D' = Just $ SpringTile Compressed (neg hu)
+monoToTile ')' = Just $ SpringTile Stretched (neg hv)
+monoToTile ']' = Just $ SpringTile Relaxed (neg hv)
+monoToTile '}' = Just $ SpringTile Compressed (neg hv)
+monoToTile '1' = Just $ SpringTile Stretched (neg hw)
+monoToTile '7' = Just $ SpringTile Relaxed (neg hw)
+monoToTile '9' = Just $ SpringTile Compressed (neg hw)
+monoToTile _ = Nothing
+
+minmax :: Ord a => [a] -> (a,a)
+minmax = minimum &&& maximum
+
+readAsciiLockFile :: FilePath -> IO (Maybe Lock, Maybe Solution)
+readAsciiLockFile path = flip catchIO (const $ return (Nothing,Nothing)) $ do
+    lines <- readStrings path
+    return $ fromMaybe (lockOfAscii lines, Nothing) $ do
+	    guard $ length lines > 2
+	    let (locklines, [header,solnLine]) = splitAt (length lines - 2) lines
+	    guard $ isPrefixOf "Solution:" header
+	    return (lockOfAscii locklines, tryRead solnLine)
+
+writeAsciiLockFile :: FilePath -> Maybe Solution -> Lock -> IO ()
+writeAsciiLockFile path msoln lock = do
+    writeStrings path $ lockToAscii lock ++ case msoln of
+	Nothing -> []
+	Just soln -> ["Solution:", show soln]
+
diff --git a/BinaryInstances.hs b/BinaryInstances.hs
new file mode 100644
--- /dev/null
+++ b/BinaryInstances.hs
@@ -0,0 +1,134 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module BinaryInstances where
+import Data.Binary
+import qualified Data.Vector as Vector
+import Control.Monad
+import Data.Int (Int8)
+
+import GameStateTypes
+import Physics
+import Hex
+import Frame
+
+newtype SmallInt = SmallInt {fromSmallInt :: Int}
+newtype SmallNat = SmallNat {fromSmallNat :: Int}
+instance Binary SmallNat where
+    put (SmallNat n) = if n < 255 then putWord8 (fromIntegral n) else putWord8 255 >> put n
+    get = do
+	n' <- get :: Get Word8
+	if n' == 255 then liftM SmallNat get else return $ SmallNat $ fromIntegral n'
+
+instance Binary SmallInt where
+    put (SmallInt n) = if abs n < 127 then put ((fromIntegral n)::Int8) else put (127::Int8) >> put n
+    get = do
+	n' <- get :: Get Int8
+	if n' == 127 then liftM SmallInt get else return $ SmallInt $ fromIntegral n'
+
+putPackedNat,putPackedInt :: Int -> Put
+putPackedNat n = put $ SmallNat n
+putPackedInt n = put $ SmallInt n
+getPackedNat,getPackedInt :: Get Int
+getPackedNat = liftM fromSmallNat get
+getPackedInt = liftM fromSmallInt get
+
+newtype ShortList a = ShortList {fromShortList :: [a]}
+instance Binary a => Binary (ShortList a) where
+    put (ShortList as) = putPackedNat (length as) >> mapM_ put as
+    get = do
+	n <- getPackedNat
+	ShortList `liftM` getMany n
+
+ -- | 'getMany n' get 'n' elements in order, without blowing the stack.
+ -- [ copied from source of package 'binary' by Lennart Kolmodin ]
+getMany :: Binary a => Int -> Get [a]
+getMany = go []
+ where
+    go xs 0 = return $! reverse xs
+    go xs i = do x <- get
+                 -- we must seq x to avoid stack overflows due to laziness in
+                 -- (>>=)
+                 x `seq` go (x:xs) (i-1)
+{-# INLINE getMany #-}
+
+instance Binary HexVec where
+    put (HexVec x y _) = putPackedInt x >> putPackedInt y
+    get = do 
+	x <- getPackedInt
+	y <- getPackedInt
+	return $ tupxy2hv (x,y)
+instance Binary g => Binary (PHS g) where
+    put (PHS v) = put v
+    get = liftM PHS get
+instance Binary GameState where
+    put (GameState pps conns) = put (ShortList $ Vector.toList pps) >> put (ShortList conns)
+    get = liftM2 GameState (liftM (Vector.fromList . fromShortList) get) (liftM fromShortList get)
+instance Binary PlacedPiece where
+    put (PlacedPiece ppos p) = put ppos >> put p
+    get = liftM2 PlacedPiece get get
+instance Binary Piece where
+    put (Block patt) = put (0::Word8) >> put (ShortList patt)
+    put (Pivot arms) = put (1::Word8) >> put (ShortList arms)
+    put (Hook arm stiffness) = put (2::Word8) >> put arm >> put stiffness
+    put (Wrench mom) = put (3::Word8) >> put mom
+    put Ball = put (4::Word8)
+    get = do
+	tag <- get :: Get Word8
+	case tag of
+	    0 -> liftM (Block . fromShortList) get
+	    1 -> liftM (Pivot . fromShortList) get
+	    2 -> liftM2 Hook get get
+	    3 -> liftM Wrench get
+	    4 -> return Ball
+instance Binary Connection where
+    put (Connection (ri,rp) (ei,ep) l) = putPackedInt ri >> put rp >> putPackedInt ei >> put ep >> put l
+    get = do
+	ri <- getPackedInt
+	rp <- get
+	ei <- getPackedInt
+	ep <- get
+	l <- get
+	return $ Connection (ri,rp) (ei,ep) l
+instance Binary Link where
+    put (Free p) = put (0::Word8) >> put p
+    put (Spring d l) = put (1::Word8) >> put d >> putPackedInt l
+    get = do
+	tag <- get :: Get Word8
+	case tag of
+	    0 -> liftM Free get
+	    1 -> liftM2 Spring get getPackedInt
+instance Binary HookForce where
+    put NullHF = put (0::Word8)
+    put (TorqueHF dir) = put (1::Word8) >> putPackedInt dir
+    put (PushHF v) = put (2::Word8) >> put v
+    get = do
+	tag <- get :: Get Word8
+	case tag of
+	    0 -> return NullHF
+	    1 -> liftM TorqueHF getPackedInt
+	    2 -> liftM PushHF get
+instance Binary Frame where
+    put (BasicFrame s) = putPackedInt s
+    get = liftM BasicFrame getPackedInt
+
+instance Binary PlayerMove where
+    put NullPM = put (0::Word8)
+    put (HookPush v) = put (1::Word8) >> put v
+    put (HookTorque dir) = put (2::Word8) >> putPackedInt dir
+    put (WrenchPush v) = put (3::Word8) >> put v
+    get = do
+	tag <- get :: Get Word8
+	case tag of
+	    0 -> return NullPM
+	    1 -> liftM HookPush get
+	    2 -> liftM HookTorque getPackedInt
+	    3 -> liftM WrenchPush get
+
diff --git a/BoardColouring.hs b/BoardColouring.hs
new file mode 100644
--- /dev/null
+++ b/BoardColouring.hs
@@ -0,0 +1,93 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module BoardColouring where
+
+import Control.Applicative hiding ((<*>))
+import Control.Monad
+import Data.Function (on)
+import qualified Data.Map as Map
+import Data.Map (Map)
+import qualified Data.Set as Set
+import Data.Set (Set)
+import qualified Data.Vector as Vector
+import Data.List
+import Data.Maybe
+
+import Hex
+import GameState
+import GameStateTypes
+import Util
+import GraphColouring
+
+type PieceColouring = Map PieceIdx Int
+
+colouredPieces :: Bool -> GameState -> [PieceIdx]
+colouredPieces colourFixed st = [ idx |
+    (idx, PlacedPiece _ p) <- enumVec $ placedPieces st
+    , isPivot p ||
+	and [ isBlock p, idx > 0
+	      , or [ colourFixed, not $ null $ springsEndAtIdx st idx ] ] ]
+
+pieceTypeColouring :: GameState -> [PieceIdx] -> PieceColouring
+pieceTypeColouring st coloured = Map.fromList
+    [ (idx, col) | (idx, PlacedPiece _ p) <- enumVec $ placedPieces st
+	, idx `elem` coloured
+	, let col = if isBlock p then 1+((connGraphHeight st idx - 1) `mod` 5) else 0 ]
+    
+
+boardColouring :: GameState -> [PieceIdx] -> PieceColouring -> PieceColouring
+boardColouring st coloured lastCol =
+   fiveColour graph lastCol
+    where
+	board = stateBoard st
+	graph = Map.fromList [ (idx, nub $ neighbours idx)
+	    | idx <- coloured ]
+	neighbours idx = 
+	    neighbours' idx (perim idx) []
+	perim :: PieceIdx -> Set (HexPos,HexDir)
+	perim idx =
+	    Set.fromList $ nubBy ((==)`on`fst) [ (pos', neg dir)
+		| dir <- hexDirs
+		, pos <- fullFootprint st idx
+		, let pos' = dir <+> pos
+		, Just True /= do
+		    (idx',_) <- Map.lookup pos' board
+		    return $ idx == idx'
+		]
+	neighbours' :: PieceIdx -> Set (HexPos,HexDir) -> [PieceIdx] -> [PieceIdx]
+	neighbours' idx as ns 
+	    | Set.null as = ns
+	    | otherwise =
+		let a = head $ Set.elems as
+		    (path, ns') = march idx (fst a) a True
+		in neighbours' idx
+		    (Set.filter (\(pos,_) -> pos `notElem` path) as)
+		    (ns++ns')
+	-- |march around the piece's boundary, returning positions visited and
+	-- neighbouring pieces met (in order)
+	march idx startPos (pos,basedir) init
+	    | not init && pos == startPos = ([],[])
+	    | otherwise =
+	    let n = do 
+		    (idx',_) <- Map.lookup pos board
+		    guard $ idx' `elem` coloured
+		    return idx'
+		mNext = listToMaybe 
+		    [ (pos', rotate (h-2) basedir)
+		    | h <- [1..5] 
+		    , let pos' = (rotate h basedir)<+>pos
+		    , (fst <$> Map.lookup pos' board) /= Just idx
+		    ]
+		(path,ns) = case mNext of
+		    Nothing -> ([],[])
+		    Just next -> march idx startPos next False
+	    in (pos:path, (maybeToList n)++ns)
+
diff --git a/COPYING b/COPYING
new file mode 100644
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/CVec.hs b/CVec.hs
new file mode 100644
--- /dev/null
+++ b/CVec.hs
@@ -0,0 +1,29 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module CVec where
+
+import Hex
+import Data.Monoid
+
+data CVec = CVec { cy, cx :: Int }
+    deriving (Eq, Ord, Show)
+instance Monoid CVec where
+    mempty = CVec 0 0
+    mappend (CVec y x) (CVec y' x') = CVec (y+y') (x+x')
+instance Grp CVec where
+    neg (CVec y x) = CVec (-y) (-x) 
+type CCoord = PHS CVec
+
+hexVec2CVec :: HexVec -> CVec
+hexVec2CVec (HexVec x y z) = CVec (-y) (x-z) 
+cVec2HexVec :: CVec -> HexVec
+cVec2HexVec (CVec y x) = HexVec ((x+y)`div`2) (-y) ((y-x)`div`2)
+
diff --git a/Cache.hs b/Cache.hs
new file mode 100644
--- /dev/null
+++ b/Cache.hs
@@ -0,0 +1,96 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module Cache where
+
+import Network.Fancy
+import Control.Concurrent.STM
+import Control.Concurrent
+import Control.Applicative
+import Control.Exception
+import Control.Monad.Trans.Reader
+import System.Directory
+import Control.Monad
+import Data.Maybe
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy as BL
+import Data.Binary
+import System.IO
+import System.FilePath
+
+import Database
+import Protocol
+import Metagame
+import Mundanities
+import ServerAddr
+
+data FetchedRecord = FetchedRecord {fresh :: Bool, fetchError :: Maybe String, fetchedRC :: Maybe RecordContents}
+    deriving (Eq, Ord, Show)
+
+
+getRecordCached :: ServerAddr -> Maybe Auth -> Maybe (TVar Bool) -> Bool -> Record -> IO (TVar FetchedRecord)
+getRecordCached saddr auth mflag cOnly rec = do
+    fromCache <- withCache saddr $ getRecord rec
+    let fresh = isJust fromCache && invariantRecord rec
+    tvar <- atomically $ newTVar $ FetchedRecord fresh Nothing fromCache
+    unless (cOnly || fresh) $ void $ forkIO $ getRecordFromServer fromCache tvar
+    return tvar
+    where
+	getRecordFromServer fromCache tvar = do
+	    let action = case rec of
+		    RecUserInfo name ->
+			let curVersion = (\(RCUserInfo (v,_)) -> v) <$> fromCache
+			in GetUserInfo name curVersion 
+		    _ -> askForRecord rec
+	    resp <- makeRequest saddr (ClientRequest
+		    protocolVersion (if needsAuth action then auth else Nothing) action)
+	    case resp of
+		ServerError err -> tellRec $ FetchedRecord True (Just err) fromCache
+		ServerCodenameFree -> tellRec $ FetchedRecord True Nothing Nothing
+		ServerFresh -> tellRec $ FetchedRecord True Nothing fromCache
+		ServedUserInfoDeltas deltas -> do
+		    let Just (RCUserInfo (v,info)) = fromCache
+		    let rc = RCUserInfo (v+length deltas, applyDeltas info deltas)
+		    withCache saddr $ putRecord rec rc
+		    tellRec $ FetchedRecord True Nothing (Just rc)
+		_ -> do
+		    let rc = rcOfServerResp resp
+		    withCache saddr $ putRecord rec rc
+		    tellRec $ FetchedRecord True Nothing (Just rc)
+	    where
+		tellRec fr = atomically $ do
+		    writeTVar tvar fr
+		    case mflag of {Just flag -> writeTVar flag True; _ -> return ()}
+
+waitFetchedFresh :: TVar FetchedRecord -> IO ()
+waitFetchedFresh tvar = atomically $ readTVar tvar >>= check.fresh
+
+makeRequest :: ServerAddr -> ClientRequest -> IO ServerResponse
+makeRequest saddr@(ServerAddr host port) request =
+    handle (return . ServerError . (show::SomeException -> String)) $
+	withStream (IP host port) makeRequest'
+	    `catchIO` (const $ return $ ServerError $ "Cannot connect to "++saddrStr saddr++"!")
+    where
+	makeRequest' hdl = do
+	    BS.hPut hdl $ BL.toStrict $ encode request 
+	    hFlush hdl
+	    (decode . BL.fromStrict) `liftM` BS.hGetContents hdl
+
+knownServers :: IO [ServerAddr]
+knownServers = flip catchIO (const $ return []) $ do
+    cachedir <- confFilePath "cache"
+    saddrstrs <- getDirectoryContents cachedir >>= filterM (\dir ->
+	doesDirectoryExist $ cachedir++[pathSeparator]++dir++[pathSeparator]++"intricacydb"++[pathSeparator])
+    return $ concat $ map (maybeToList . strToSaddr) saddrstrs
+
+withCache :: ServerAddr -> DBM a -> IO a
+withCache saddr m = do
+    cachedir <- (++[pathSeparator]++saddrStr saddr) `liftM` confFilePath "cache"
+    runReaderT m cachedir
diff --git a/Command.hs b/Command.hs
new file mode 100644
--- /dev/null
+++ b/Command.hs
@@ -0,0 +1,112 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module Command where
+import Hex
+import GameStateTypes
+import Metagame
+
+data Command = CmdDir WrHoSel HexDir | CmdRotate TorqueDir | CmdWait
+	| CmdMoveTo HexPos | CmdManipulateToolAt HexPos
+	| CmdDrag HexPos HexDir
+	| CmdToggle
+	| CmdOpen
+	| CmdTile Tile | CmdPaint (Maybe Tile)
+	| CmdPaintFromTo (Maybe Tile) HexPos HexPos
+	| CmdSelect | CmdUnselect
+	| CmdDelete | CmdMerge
+	| CmdMark | CmdJumpMark
+	| CmdPlay | CmdTest
+	| CmdUndo | CmdRedo
+	| CmdReplayForward Int | CmdReplayBack Int
+	| CmdInputChar Char
+	| CmdInputSelLock LockIndex | CmdInputCodename Codename | CmdInputSelUndecl Undeclared
+	| CmdWriteState
+	| CmdTutorials
+	| CmdShowRetired | CmdPlayLockSpec (Maybe LockSpec)
+	| CmdSetServer | CmdToggleCacheOnly
+	| CmdSelCodename (Maybe Codename) | CmdBackCodename | CmdHome
+	| CmdSolve (Maybe LockIndex) | CmdDeclare (Maybe Undeclared)
+	| CmdViewSolution (Maybe NoteInfo)
+	| CmdSelectLock | CmdNextLock | CmdPrevLock
+	| CmdEdit | CmdPlaceLock (Maybe LockIndex)
+	| CmdRegister | CmdAuth
+	| CmdNextPage | CmdPrevPage
+	| CmdRedraw | CmdRefresh | CmdSuspend
+	| CmdQuit | CmdForceQuit
+	| CmdHelp | CmdBind
+	| CmdNone
+    deriving (Eq, Ord, Show, Read)
+data WrHoSel = WHSWrench | WHSHook | WHSSelected
+    deriving (Eq, Ord, Show, Read)
+	
+describeCommand :: Command -> String
+describeCommand (CmdDir whs dir) = "move " ++ whsStr whs ++ " " ++ dirStr dir
+describeCommand (CmdRotate dir) = "rotate " ++ (if dir == 1 then "counter" else "") ++ "clockwise"
+describeCommand CmdWait = "nothing"
+describeCommand CmdToggle = "toggle tool"
+describeCommand CmdOpen = "open lock"
+describeCommand (CmdTile tile) = tileStr tile
+describeCommand CmdMerge = "merge with adjacent piece" 
+describeCommand CmdMark = "mark state"
+describeCommand CmdJumpMark = "jump to marked state"
+describeCommand CmdSelect = "select piece" 
+describeCommand CmdUnselect = "unselect piece"
+describeCommand CmdDelete = "delete piece"
+describeCommand CmdPlay = "play lock"
+describeCommand CmdTest = "test lock"
+describeCommand CmdUndo = "undo"
+describeCommand CmdRedo = "redo"
+describeCommand (CmdReplayForward _) = "advance replay"
+describeCommand (CmdReplayBack _) = "rewind replay"
+describeCommand CmdWriteState = "write lock"
+describeCommand CmdTutorials = "play tutorial levels"
+describeCommand CmdShowRetired = "show retired locks"
+describeCommand CmdSetServer = "set server"
+describeCommand CmdToggleCacheOnly = "toggle using only cache"
+describeCommand (CmdSelCodename mname) = "select player" ++ maybe "" (' ':) mname
+describeCommand CmdBackCodename = "select last player"
+describeCommand CmdHome = "select self"
+describeCommand (CmdSolve mli) = "solve lock" ++ maybe "" ((' ':).(:"").lockIndexChar) mli
+describeCommand (CmdPlayLockSpec mls) = "find lock by number" ++ maybe "" ((' ':).show) mls
+describeCommand (CmdDeclare mundecl) = "declare solution" ++ maybe "" (\_->" [specified solution]") mundecl
+describeCommand (CmdViewSolution mnote) = "view lock solution" ++ maybe "" (\_->" [specified solution]") mnote
+describeCommand CmdSelectLock = "choose lock"
+describeCommand CmdNextLock = "next lock"
+describeCommand CmdPrevLock = "previous lock"
+describeCommand CmdNextPage = "page forward through lists"
+describeCommand CmdPrevPage = "page back through lists"
+describeCommand CmdEdit = "edit lock"
+describeCommand (CmdPlaceLock mli) = "place lock" ++ maybe "" ((' ':).(:"").lockIndexChar) mli
+describeCommand CmdRegister = "register codename"
+describeCommand CmdAuth = "authenticate"
+describeCommand CmdBind = "bind key"
+describeCommand CmdQuit = "quit"
+describeCommand CmdHelp = "help"
+describeCommand _ = ""
+
+tileStr HookTile = "hook"
+tileStr (WrenchTile _) = "wrench"
+tileStr (ArmTile _ _) = "arm"
+tileStr (PivotTile _) = "pivot"
+tileStr (SpringTile _ _) = "spring"
+tileStr (BlockTile _) = "block"
+tileStr BallTile = "ball"
+whsStr WHSWrench = "wrench"
+whsStr WHSHook = "hook"
+whsStr WHSSelected = "tool"
+dirStr v
+    | v == hu = "right"
+    | v == neg hu = "left"
+    | v == hv = "up-left"
+    | v == neg hv = "down-right"
+    | v == hw = "down-left"
+    | v == neg hw = "up-right"
+dirStr _ = ""
diff --git a/CursesRender.hs b/CursesRender.hs
new file mode 100644
--- /dev/null
+++ b/CursesRender.hs
@@ -0,0 +1,98 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module CursesRender where
+
+import qualified UI.HSCurses.Curses as Curses
+import Data.Char (ord)
+import qualified Data.Map as Map
+import Data.Map (Map)
+
+import Hex
+import CVec
+import GameStateTypes
+import BoardColouring (PieceColouring)
+
+-- From Curses.CursesHelper:
+-- | Converts a list of 'Curses.Color' pairs (foreground color and
+--   background color) into the curses representation 'Curses.Pair'.
+colorsToPairs :: [(Curses.Color, Curses.Color)] -> IO [Curses.Pair]
+colorsToPairs cs =
+    do p <- Curses.colorPairs
+       let nColors = length cs
+           blackWhite = p < nColors
+       if blackWhite then do
+	    print ("Terminal does not support enough colors. Number of " ++
+                      " colors requested: " ++ show nColors ++
+                      ". Number of colors supported: " ++ show p)
+	    return $ replicate nColors $ Curses.Pair 0
+          else mapM toPairs (zip [1..] cs)
+     where toPairs (n, (fg, bg)) =
+               let p = Curses.Pair n
+               in do Curses.initPair p fg bg
+                     return p
+
+type AttrChar = (Char, Curses.Attr)
+type ColPair = Int
+white,red,green,yellow,blue,magenta,cyan :: ColPair
+white = 0
+red = 1
+green = 2
+yellow = 3
+blue = 4
+magenta = 5
+cyan = 6
+data Glyph = Glyph Char ColPair Curses.Attr
+a0 = Curses.attr0
+bold = Curses.setBold a0 True
+tileChar :: Tile -> AttrChar
+tileChar (BlockTile _) = ('#',a0)
+tileChar (PivotTile dir) | dir == zero = ('o',bold)
+    | canonDir dir == hu = ('-',bold)
+    | canonDir dir == hv = ('\\',bold)
+    | canonDir dir == hw = ('/',bold)
+tileChar (ArmTile dir principal) = let
+    cdir = canonDir dir
+    c | cdir == hu = '-'
+      | cdir == hv = '\\'
+      | cdir == hw = '/'
+      | otherwise = '?'
+    a = if principal then bold else a0
+    in (c,a)
+tileChar HookTile = ('@',bold)
+tileChar (WrenchTile mom) = ('*',if mom /= zero then bold else a0)
+tileChar BallTile = ('O',a0)
+tileChar (SpringTile Relaxed _) = ('S',a0)
+tileChar (SpringTile Compressed _) = ('$',bold)
+tileChar (SpringTile Stretched _) = ('s',bold)
+tileChar _ = ('?',bold)
+
+ownedTileGlyph :: PieceColouring -> [PieceIdx] -> OwnedTile -> Glyph
+ownedTileGlyph colouring reversed (owner,t) =
+    let (ch,attr) = tileChar t
+	pair = case Map.lookup owner colouring of
+		    Nothing -> 0
+		    Just n -> n+1
+	rev = owner `elem` reversed
+    in Glyph ch pair (Curses.setReverse attr rev)
+
+addCh :: Char ->  IO ()
+addCh c = Curses.wAddStr Curses.stdScr [c]
+mvAddCh :: CVec -> Char ->  IO ()
+mvAddCh (CVec y x) c = Curses.mvAddCh y x $ fromIntegral $ ord c
+mvAddStr :: CVec -> String -> IO ()
+mvAddStr (CVec y x) = Curses.mvWAddStr Curses.stdScr y x
+mvAddGlyph :: [Curses.Pair] -> CVec -> Glyph -> IO ()
+mvAddGlyph cpairs v (Glyph ch col attr) =
+    Curses.attrSet attr (cpairs!!col) >> mvAddCh v ch
+move :: CVec -> IO ()
+move (CVec y x) = Curses.move y x
+clearLine :: Int -> IO ()
+clearLine y = Curses.move y 0 >> Curses.clrToEol
diff --git a/CursesUI.hs b/CursesUI.hs
new file mode 100644
--- /dev/null
+++ b/CursesUI.hs
@@ -0,0 +1,140 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module CursesUI where
+
+import qualified UI.HSCurses.Curses as Curses
+import Control.Concurrent.STM
+import Control.Applicative
+import qualified Data.Map as Map
+import Data.Map (Map)
+import Data.Array
+import Data.Maybe
+import Data.List
+import Control.Monad.Trans.Maybe
+import Control.Monad.State
+import Data.Function (on)
+
+import Hex
+import GameState (stateBoard)
+import GameStateTypes
+import BoardColouring
+import Frame
+import KeyBindings
+import Command
+import Mundanities
+import ServerAddr
+import InputMode
+import CursesRender
+import CVec
+
+data UIState = UIState { dispCPairs::[Curses.Pair], dispCentre::HexPos
+    , dispLastCol::PieceColouring, uiKeyBindings :: Map InputMode KeyBindings
+    , message::Maybe (Curses.Attr, ColPair, String)}
+type UIM = StateT UIState IO
+nullUIState = UIState [] (PHS zero) Map.empty Map.empty Nothing
+
+readBindings :: UIM ()
+readBindings = do 
+    path <- liftIO $ confFilePath "bindings"
+    mbdgs <- liftIO $ readReadFile path
+    case mbdgs of
+	Just bdgs -> modify $ \s -> s {uiKeyBindings = bdgs}
+	Nothing -> return ()
+writeBindings :: UIM ()
+writeBindings = do
+    path <- liftIO $ confFilePath "bindings"
+    bdgs <- gets uiKeyBindings
+    liftIO makeConfDir
+    liftIO $ writeFile path $ show bdgs
+
+getBindings mode = do
+    uibdgs <- Map.findWithDefault [] mode `liftM` gets uiKeyBindings
+    return $ uibdgs ++ bindings mode
+
+bindingsStr :: InputMode -> [Command] -> UIM String
+bindingsStr mode cmds = do
+    uibdgs <- Map.findWithDefault [] mode `liftM` gets uiKeyBindings
+    return $ (\x -> "["++x++"]") $ intercalate "," $
+	[ maybe "" showKey $ findBinding (uibdgs ++ bindings mode) cmd
+	    | cmd <- cmds ]
+
+
+erase :: UIM ()
+erase = liftIO Curses.erase
+refresh :: UIM ()
+refresh = liftIO Curses.refresh
+
+type Geom = (CVec, HexPos)
+getGeom :: UIM Geom
+getGeom = do 
+    (h,w) <- liftIO Curses.scrSize
+    centre <- gets dispCentre
+    return (CVec (h`div`2) (w`div`2), centre)
+
+drawAt :: Glyph -> HexPos -> UIM ()
+drawAt gl pos =
+    drawAtWithGeom gl pos =<< getGeom
+drawAtWithGeom gl pos geom@(scrCentre,centre) =
+    drawAtCVec gl $ scrCentre <+> (hexVec2CVec $ pos <-> centre)
+drawAtCVec :: Glyph -> CVec -> UIM ()
+drawAtCVec gl cpos = do
+    cpairs <- gets dispCPairs
+    liftIO $ mvAddGlyph cpairs cpos gl
+
+drawStr :: Curses.Attr -> ColPair -> CVec -> String -> UIM ()
+drawStr attr col v str = do
+    cpairs <- gets dispCPairs
+    liftIO $ Curses.attrSet attr (cpairs!!col) >> mvAddStr v str
+drawStrGrey :: CVec -> String -> UIM ()
+drawStrGrey = drawStr a0 0
+drawStrCentred attr col v str =
+    drawStr attr col (v <+> CVec 0 (-length str `div` 2)) str
+
+drawCursorAt :: Maybe HexPos -> UIM ()
+drawCursorAt Nothing =
+    void $ liftIO $ Curses.cursSet Curses.CursorInvisible
+drawCursorAt (Just pos) = do 
+    geom@(scrCentre,centre) <- getGeom
+    liftIO $ Curses.cursSet Curses.CursorVisible
+    liftIO $ move $ scrCentre <+> (hexVec2CVec $ pos <-> centre)
+
+drawState :: [PieceIdx] -> Bool -> [Alert] -> GameState -> UIM ()
+drawState reversed colourFixed alerts st = do
+    lastCol <- gets dispLastCol
+    colouring <- drawStateWithGeom reversed colourFixed lastCol st =<< getGeom
+    modify $ \ds -> ds { dispLastCol = colouring }
+
+-- TODO: monochrome mode
+drawStateWithGeom reversed colourFixed lastCol st geom = do
+    let colouring = boardColouring st (colouredPieces colourFixed st) lastCol
+    sequence_ [ drawAtWithGeom glyph pos geom | 
+	(pos,glyph) <- Map.toList $ fmap (ownedTileGlyph colouring reversed) $ stateBoard st
+	]
+    return colouring
+
+drawMsgLine = void.runMaybeT $ do
+    (attr,col,str) <- MaybeT $ gets message
+    lift $ do
+	(h,w) <- liftIO Curses.scrSize
+	liftIO $ clearLine $ h-1
+	drawStr attr col (CVec (h-1) 0) $ take (w-1) str
+setMsgLine attr col str = do
+    modify $ \s -> s { message = Just (attr,col,str) }
+    drawMsgLine
+    refresh
+
+drawTitle (Just title) = do
+    (h,w) <- liftIO Curses.scrSize
+    drawStrCentred a0 white (CVec 0 (w`div`2)) title
+drawTitle Nothing = return ()
+    
+say = setMsgLine bold white
+sayError = setMsgLine bold red
diff --git a/CursesUIMInstance.hs b/CursesUIMInstance.hs
new file mode 100644
--- /dev/null
+++ b/CursesUIMInstance.hs
@@ -0,0 +1,343 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+{-# LANGUAGE FlexibleInstances #-}
+module CursesUIMInstance () where
+
+import qualified UI.HSCurses.Curses as Curses
+import qualified UI.HSCurses.CursesHelper as CursesH
+import Control.Concurrent.STM
+import Control.Applicative
+import Data.Char (ord)
+import Data.Monoid
+import qualified Data.Map as Map
+import Data.Map (Map)
+import Data.Array
+import Data.Maybe
+import Data.List
+import Control.Monad.Trans.Maybe
+import Control.Concurrent (threadDelay)
+import Control.Monad.State
+import Data.Function (on)
+import Data.Foldable (for_)
+
+import Hex
+import GameStateTypes
+import Physics
+import Frame
+import Command
+import KeyBindings
+import ServerAddr
+import Cache
+import Database
+import Metagame
+import Protocol
+import Util
+import InputMode
+import MainState
+import CursesUI
+import CursesRender
+import CVec
+
+
+drawName :: Bool -> CVec -> Codename -> StateT MainState UIM ()
+drawName showScore pos name = do
+    ourName <- (authUser <$>) <$> gets curAuth
+    relScore <- getRelScore name
+    let (attr,col) = case relScore of
+	    Just 0 -> (a0,yellow)
+	    Just 1 -> (bold,cyan) 
+	    Just 2 -> (a0,green) 
+	    Just 3 -> (bold,green) 
+	    Just (-1) -> (bold,magenta)
+	    Just (-2) -> (a0,red)
+	    Just (-3) -> (bold,red)
+	    _ -> if ourName == Just name then (bold,white) else (a0,white)
+    lift $ drawStrCentred attr col pos
+	(name ++ if showScore then " " ++ maybe "" show relScore else "")
+
+drawActiveLock :: CVec -> ActiveLock -> StateT MainState UIM ()
+drawActiveLock pos (ActiveLock name i) = do
+    drawName False (pos <+> CVec 0 (-1)) name
+    lift $ drawStr bold white (pos <+> CVec 0 1) [':',lockIndexChar i]
+
+data Gravity = GravUp | GravLeft | GravRight | GravDown | GravCentre
+    deriving (Eq, Ord, Show, Enum)
+fillBox :: CVec -> CVec -> Int -> Gravity -> [CVec -> StateT MainState UIM ()] -> StateT MainState UIM Int
+fillBox (CVec t l) (CVec b r) width grav draws = do
+    let half = width`div`2
+	starty = (if grav == GravDown then b else t)
+	cv = (b+t)`div`2
+	ch = (l+r)`div`2
+	gravCentre = case grav of
+	    GravDown   -> CVec b ch
+	    GravUp     -> CVec t ch
+	    GravLeft   -> CVec cv l
+	    GravRight  -> CVec cv r
+	    GravCentre -> CVec cv ch
+	locs = sortBy (compare `on` dist) $ concat
+	    [ map (CVec j) [ l+margin + (width+1)*i | i <- [0..(r-l-(2*margin))`div`(width+1)] ]
+	    | j <- [t..b]
+	    , let margin = if (j-starty)`mod`2 == 0 then half else width ]
+	dist v = sqlen $ v <-> gravCentre
+	sqlen (CVec y x) = (y*(width+1))^2+x^2
+    selDraws <- do
+	    offset <- gets listOffset
+	    let na = length locs
+		nd = length draws
+	    return $ drop (max 0 $ min (nd - na) (na*offset)) $ draws
+    let zipped = zip locs selDraws
+    sequence_ $ map (uncurry ($)) $ zip selDraws locs
+    return $ (if grav==GravDown then (minimum.(b:)) else (maximum.(t:))) [ y | (CVec y x,_) <- zipped ]
+
+drawLockInfo al@(ActiveLock name i) lockinfo = do
+    (h,w) <- liftIO Curses.scrSize
+    let [left,vcentre,right] = [ (k+2*i)*w`div`6 + (1-k) | k <- [0,1,2] ]
+    let [top,bottom] = [6, h-1]
+    let hcentre = (top+bottom)`div`2 - 1
+    ourName <- (authUser <$>) <$> gets curAuth
+
+    (lockTop, lockBottom) <- (fromJust<$>)$ runMaybeT $ msum
+	[ do
+	    lock <- mgetLock $ lockSpec lockinfo
+	    let size = frameSize $ fst lock
+	    guard $ bottom - top >= 5 + 2*size+1 + 1 + 5 && right-left >= 4*size+1
+	    lift.lift $ drawStateWithGeom [] False Map.empty (snd lock) (CVec hcentre vcentre,origin)
+	    return (hcentre - size - 1, hcentre + size + 1)
+	, lift $ do
+	    drawActiveLock (CVec hcentre vcentre) al
+	    return (hcentre - 1, hcentre + 1)
+	]
+
+    startOn <-
+	if (public lockinfo) 
+	then lift $ drawStrCentred bold white (CVec (lockTop-1) vcentre) "Everyone!"
+	    >> return (lockTop-1)
+	else if null $ accessedBy lockinfo
+	    then lift $ drawStrCentred a0 white (CVec (lockTop-1) vcentre) "No-one"
+		>> return (lockTop-1)
+	    else
+		fillBox (CVec (top+1) (left+1)) (CVec (lockTop-1) (right-1)) 3 GravDown $
+		    [ \pos -> drawName False pos name | name <- accessedBy lockinfo ]
+    lift $ drawStrCentred a0 white (CVec (startOn-1) vcentre) "Accessed by:"
+    
+    undecls <- gets undeclareds
+    if isJust $ guard . (|| public lockinfo) . (`elem` accessedBy lockinfo) =<< ourName
+    then lift $ drawStrCentred a0 green (CVec (lockBottom+1) vcentre) "Accessed!"
+    else if any (\(Undeclared _ ls _) -> ls == lockSpec lockinfo) undecls
+    then lift $ drawStrCentred a0 yellow (CVec (lockBottom+1) vcentre) "Undeclared solution!"
+    else do
+	read <- take 3 <$> getNotesReadOn lockinfo
+	unless (null read || ourName == Just name) $ do
+	    let rntext = if right-left > 30 then "Read notes by:" else "Notes:"
+		s = vcentre - (length rntext+(3+1)*3)`div`2
+	    lift $ drawStr a0 white (CVec (lockBottom+1) s) rntext
+	    void $ fillBox (CVec (lockBottom+1) (s+length rntext+1)) (CVec (lockBottom+1) right) 3 GravLeft
+		[ \pos -> drawName False pos name | name <- map noteAuthor read ]
+
+    lift $ drawStrCentred a0 white (CVec (lockBottom+2) vcentre) "Notes held:"
+    if null $ notesSecured lockinfo
+	then lift $
+	    drawStrCentred a0 white (CVec (lockBottom+3) vcentre) "None"
+	else
+	    void $ fillBox (CVec (lockBottom+3) (left+1)) (CVec bottom (right-1)) 5 GravUp
+		[ (`drawActiveLock` al) | al <- map noteOn $ notesSecured lockinfo ]
+
+charify :: Curses.Key -> Maybe Char
+charify key = case key of
+    Curses.KeyChar ch -> Just ch
+    Curses.KeyBackspace -> Just '\b'
+    Curses.KeyLeft -> Just '4'
+    Curses.KeyRight -> Just '6'
+    Curses.KeyDown -> Just '2'
+    Curses.KeyUp -> Just '8'
+    Curses.KeyHome -> Just '7'
+    Curses.KeyNPage -> Just '3'
+    Curses.KeyPPage -> Just '9'
+    Curses.KeyEnd -> Just '1'
+    _ -> Nothing
+
+instance UIMonad (StateT UIState IO) where
+    runUI m = evalStateT m nullUIState
+
+    drawMainState = do
+	lift erase
+	s <- get
+	lift . drawTitle =<< getTitle
+	lift drawMsgLine
+	drawMainState' s
+	lift refresh
+	where
+	drawMainState' (PlayState { psCurrentState=st, psLastAlerts=alerts, wrenchSelected=wsel }) = lift $ do
+	    drawState [] False alerts st
+	    drawCursorAt $ listToMaybe [ pos |
+		(_, PlacedPiece pos p) <- enumVec $ placedPieces st
+		, or [wsel && isWrench p, not wsel && isHook p] ]
+	drawMainState' (ReplayState {}) = do
+	    lift . drawState [] False [] =<< gets rsCurrentState
+	    lift $ drawCursorAt Nothing
+	drawMainState' (EditState { esGameStateStack=(st:_), selectedPiece=selPiece, selectedPos=selPos }) = lift $ do
+	    drawState (maybeToList selPiece) True [] st
+	    drawCursorAt $ if isNothing selPiece then Just selPos else Nothing
+	drawMainState' (MetaState saddr undecls cOnly auth names _ _ rnamestvar _ _ mretired path lock _) = do
+	    let ourName = liftM authUser auth
+	    let selName = listToMaybe names
+	    (h,w) <- liftIO Curses.scrSize
+	    when (h<20 || w<40) $ liftIO CursesH.end >> error "Terminal too small!"
+	    lift $ do
+		drawCursorAt Nothing
+		sstr <- (++"   Server: ") <$> bindingsStr IMMeta [CmdSetServer, CmdToggleCacheOnly]
+		hstr <- (\[ks,ts] -> ts++" tut   "++ks++" keys") <$>
+		    bindingsStr IMMeta `mapM` [[CmdHelp],[CmdTutorials]]
+		drawStrGrey (CVec 0 0) $ sstr ++
+		    take (w - length sstr - length hstr)
+			(saddrStr saddr ++ (if cOnly then "  (cache only) " else "") ++ repeat ' ') ++
+		    hstr
+		drawStrGrey (CVec 1 0) =<<
+		    (\[es,ls] -> take 5 (es ++ repeat ' ') ++ "   Lock: " ++ path ++ "     " ++ ls) <$>
+			bindingsStr IMMeta `mapM`
+			    [ [CmdEdit] ++ if path == "" then [] else [CmdPlaceLock Nothing]
+			    , [CmdSelectLock] ++ if path == "" then [] else [CmdNextLock, CmdPrevLock]
+			    ]
+	    lift $ drawStrGrey (CVec 2 (maximum [w`div`3+1, w`div`2 - 13])) =<< bindingsStr IMMeta [CmdSelCodename Nothing]
+	    maybe (return ()) (drawName True (CVec 2 (w`div`2))) selName
+	    void.runMaybeT $ MaybeT (return selName) >>= lift . getUInfoFetched 300 >>=
+		\(FetchedRecord fresh err muirc) -> lift $ do
+		    lift $ do
+			unless fresh $ drawAtCVec (Glyph '*' red bold) $ CVec 2 (w`div`2+7)
+			maybe (return ()) sayError err
+			when (fresh && isNothing ourName) $
+			    drawStrGrey (CVec 2 (w`div`2+1+9)) =<<
+				(bindingsStr IMMeta $ if isNothing muirc then [CmdRegister] else [CmdAuth])
+		    for_ muirc $ \(RCUserInfo (_,uinfo)) -> case mretired of
+			Just retired -> do
+			    (h,w) <- liftIO Curses.scrSize
+			    void $ fillBox (CVec 6 2) (CVec (h-1) (w-2)) 5 GravCentre
+				[ \pos -> lift $ drawStrGrey pos $ show ls | ls <- retired ]
+			    lift $ drawStrGrey (CVec 5 (w`div`3)) =<< bindingsStr IMMeta
+				([CmdShowRetired] ++ if null retired then [] else [CmdPlayLockSpec Nothing])
+			Nothing -> do
+			    sequence_ [ drawLockInfo (ActiveLock (codename uinfo) i) lockinfo |
+				(i,Just lockinfo) <- assocs $ userLocks uinfo ]
+			    unless (null $ elems $ userLocks uinfo) $ lift $
+				drawStrGrey (CVec 5 (w`div`3)) =<< bindingsStr IMMeta [CmdSolve Nothing, CmdViewSolution Nothing]
+	    when (isJust ourName && ourName == selName) $ do
+		unless (null undecls) $ do
+		    str <- lift $ (++" Undeclared solutions:") <$> bindingsStr IMMeta [CmdDeclare Nothing]
+		    let s = max 1 $ (w-(length str + 6*length undecls))`div`2
+		    let y = 4
+		    lift $ drawStr bold white (CVec y s) str
+		    void $ fillBox (CVec y (s+length str+1)) (CVec y (w-1)) 5 GravLeft
+			[ (`drawActiveLock` al) | Undeclared _ _ al <- undecls ]
+		rnames <- liftIO $ atomically $ readTVar rnamestvar
+		unless (null rnames) $
+		    void $ fillBox (CVec 2 5) (CVec 5 (w`div`3)) 3 GravCentre
+			[ \pos -> drawName False pos name | name <- rnames ]
+	    when (ourName /= selName) $ void $ runMaybeT $ do
+		sel <- MaybeT $ return selName
+		us <- MaybeT $ return ourName
+		ourUInfo <- mgetUInfo us
+		let accessed = [ ActiveLock us i 
+			| i<-[0..2]
+			, Just lock <- [ userLocks ourUInfo ! i ]
+			, public lock || selName `elem` map Just (accessedBy lock) ]
+		guard $ not $ null accessed
+		let str = "has accessed:"
+		let s = (w-(4 + length str + 6*(length accessed)))`div`2
+		let y = 4
+		lift $ do
+		    drawName False (CVec y (s+1)) sel
+		    lift $ drawStrGrey (CVec y $ s+4) str
+		    void $ fillBox (CVec y (s+4+length str+1)) (CVec y (w-1)) 5 GravLeft $
+			[ (`drawActiveLock` al) | al <- accessed]
+
+    drawAlerts alerts =
+	do mapM_ drawAlert alerts
+	   unless (null alerts) 
+	    $ do refresh
+		 liftIO $ threadDelay $ 5*10^4
+	where
+	    drawAlert (AlertCollision pos) = drawAt cGlyph pos
+	    drawAlert _ = return ()
+	    cGlyph = Glyph '!' 0 a0
+
+    drawMessage = say
+    drawError = sayError
+
+    showHelp mode = do
+	bdgs <- nub <$> getBindings mode
+	erase
+	(h,w) <- liftIO Curses.scrSize
+	let bdgWidth = 35
+	    showKeys chs = intercalate "/" (map showKey chs)
+	    maxkeyslen = maximum $ map (length.showKeys.map fst) $ groupBy ((==) `on` snd) bdgs
+	drawStrCentred a0 white (CVec 0 (w`div`2)) "Bindings:"
+	sequence_ [ drawStr a0 white (CVec (y+2) (x*bdgWidth) ) $
+		keysStr ++ replicate pad ' ' ++ ": " ++ desc |
+	    ((keysStr,pad,desc),(x,y)) <- zip [(keysStr,pad,desc)
+		    | group <- groupBy ((==) `on` snd) $ sortBy (compare `on` snd) bdgs
+		    , let cmd = snd $ head group
+		    , let desc = describeCommand cmd
+		    , not $ null desc
+		    , let chs = map fst group
+		    , let keysStr = showKeys chs
+		    , let pad = max 0 $ minimum [maxkeyslen + 1 - length keysStr,
+			    bdgWidth - length desc - length keysStr - 1]
+		    ]
+		(map (`divMod` (h-3)) [0..])
+	    , (x+1)*bdgWidth < w]
+	refresh
+	void $ getInput mode
+
+    getChRaw = (charify<$>) $ liftIO $ CursesH.getKey (return ())
+    setUIBinding mode cmd ch =
+	modify $ \s -> s { uiKeyBindings =
+		Map.insertWith (\[bdg] -> \bdgs -> if bdg `elem` bdgs then delete bdg bdgs else bdgs ++ [bdg])
+		    mode [(ch,cmd)] $ uiKeyBindings s }
+
+    initUI = 
+	do liftIO CursesH.start 
+	   cpairs <- liftIO $ colorsToPairs [ (f, CursesH.black) | f <-
+		[ CursesH.white, CursesH.red, CursesH.green, CursesH.yellow, CursesH.blue, CursesH.magenta, CursesH.cyan] ] 
+	   modify $ \s -> s {dispCPairs = cpairs}
+	   readBindings
+	   return True
+
+    endUI = do
+	writeBindings
+	liftIO CursesH.end
+    unblockInput = return $ Curses.ungetCh 0
+    suspend = do
+	liftIO $ do
+	    CursesH.suspend 
+	    Curses.resetParams 
+	redraw
+    redraw = liftIO $ do
+	Curses.endWin
+	Curses.refresh
+
+    warpPointer _ = return ()
+
+    getDrawImpatience = return $ \_ -> return ()
+
+    getInput mode = do
+	let userResizeCode = 1337  -- XXX: chosen not to conflict with HSCurses codes
+	key <- liftIO $ CursesH.getKey (Curses.ungetCh userResizeCode)
+	if key == Curses.KeyUnknown userResizeCode
+	    then do
+		liftIO Curses.scrSize
+		return [CmdRedraw]
+	    else do
+		let mch = charify key
+		    unblockBinding = (toEnum 0, CmdRefresh) -- c.f. unblockInput above
+		flip (maybe $ return []) mch $ \ch ->
+		    if mode == IMTextInput
+		    then return [CmdInputChar ch]
+		    else (maybeToList . lookup ch . (unblockBinding:)) <$> getBindings mode
diff --git a/Database.hs b/Database.hs
new file mode 100644
--- /dev/null
+++ b/Database.hs
@@ -0,0 +1,201 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module Database where
+
+-- TODO: use ByteString everywhere
+
+import Control.Applicative
+import Control.Monad
+import System.IO
+import System.FilePath
+import System.Directory
+import Control.Monad.Trans.Reader
+import Control.Monad.IO.Class
+import qualified Data.ByteString.Lazy.Char8 as CL
+import qualified Data.ByteString.Char8 as CS
+
+--import OpenSSL.Digest (MessageDigest(SHA1), toHex)
+--import OpenSSL.Digest.ByteString.Lazy (digest)
+--import Data.Digest.Pure.SHA (sha1, showDigest)
+--see also OpenSSL.EVP.Digest from HsOpenSSL
+import Crypto.Hash (hashlazy, Digest, SHA1, digestToHexByteString)
+
+import Protocol
+import Metagame
+import Lock
+import Mundanities
+
+--hash :: String -> IO String
+--hash str = fmap (>>=toHex) $ digest SHA1 $ CL.pack $ str
+sha1 :: CL.ByteString -> Digest SHA1
+sha1 = hashlazy
+hash :: String -> String
+hash = CS.unpack . digestToHexByteString . sha1 . CL.pack
+
+
+data Record
+    = RecPassword Codename
+    | RecUserInfo Codename
+    | RecUserInfoLog Codename
+    | RecLock LockSpec
+    | RecNote NoteInfo
+    | RecLockHashes
+    | RecRetiredLocks Codename
+    | RecServerInfo
+    deriving (Eq, Ord, Show)
+data RecordContents
+    = RCPassword Password
+    | RCUserInfo VersionedUInfo
+    | RCUserInfoDeltas [UserInfoDelta]
+    | RCLock Lock
+    | RCSolution Solution
+    | RCLockHashes [String]
+    | RCLockSpecs [LockSpec]
+    | RCServerInfo ServerInfo
+    deriving (Eq, Ord, Show)
+
+rcOfServerResp (ServedServerInfo x) = RCServerInfo x
+rcOfServerResp (ServedLock x) = RCLock x
+rcOfServerResp (ServedSolution x) = RCSolution x
+rcOfServerResp (ServedUserInfo x) = RCUserInfo x
+rcOfServerResp (ServedRetired x) = RCLockSpecs x
+rcOfServerResp _ = error "no corresponding rc"
+
+invariantRecord (RecUserInfo _) = False
+invariantRecord (RecUserInfoLog _) = False
+invariantRecord (RecPassword _) = False
+invariantRecord (RecRetiredLocks _) = False
+invariantRecord (RecNote _) = False
+invariantRecord _ = True
+
+askForRecord RecServerInfo = GetServerInfo
+askForRecord (RecUserInfo name) = GetUserInfo name Nothing
+askForRecord (RecLock ls) = GetLock ls
+askForRecord (RecNote note) = GetSolution note
+askForRecord (RecRetiredLocks name) = GetRetired name
+askForRecord _ = error "no corresponding request"
+
+type DBM = ReaderT FilePath IO
+	    
+recordExists :: Record -> DBM Bool
+recordExists rec = recordPath rec >>= liftIO . doesFileExist
+
+getRecord :: Record -> DBM (Maybe RecordContents)
+getRecord rec = do
+    path <- recordPath rec
+    liftIO $ flip catchIO (const $ return Nothing) $ do
+	h <- openFile path ReadMode
+	getRecordh rec h <* hClose h
+getRecordh (RecPassword _) h = ((RCPassword <$>) . tryRead) <$> hGetStrict h
+getRecordh (RecUserInfo _) h = ((RCUserInfo <$>) . tryRead) <$> hGetStrict h
+getRecordh (RecUserInfoLog _) h = ((RCUserInfoDeltas <$>) . tryRead) <$> hGetStrict h
+getRecordh (RecLock _) h = ((RCLock <$>) . tryRead) <$> hGetStrict h
+getRecordh (RecNote _) h = ((RCSolution <$>) . tryRead) <$> hGetStrict h
+getRecordh RecLockHashes h = ((RCLockHashes <$>) . tryRead) <$> hGetStrict h
+getRecordh (RecRetiredLocks name) h = ((RCLockSpecs <$>) . tryRead) <$> hGetStrict h
+getRecordh RecServerInfo h = ((RCServerInfo <$>) . tryRead) <$> hGetStrict h
+
+hGetStrict h = CS.unpack `liftM` concatMWhileNonempty (repeat $ CS.hGet h 1024)
+    where concatMWhileNonempty (m:ms) = do
+	    bs <- m
+	    if CS.null bs
+		then return bs
+		else (bs `CS.append`) `liftM` concatMWhileNonempty ms
+
+putRecord :: Record -> RecordContents -> DBM ()
+putRecord rec rc = do
+    path <- recordPath rec
+    liftIO $ do
+	mkdirhierto path 
+	h <- openFile path WriteMode 
+	putRecordh rc h
+	hClose h
+putRecordh (RCPassword hpw) h = hPutStr h $ show hpw
+putRecordh (RCUserInfo info) h = hPutStr h $ show info
+putRecordh (RCUserInfoDeltas deltas) h = hPutStr h $ show deltas
+putRecordh (RCLock lock) h = hPutStr h $ show lock
+putRecordh (RCSolution solution) h = hPutStr h $ show solution
+putRecordh (RCLockHashes hashes) h = hPutStr h $ show hashes
+putRecordh (RCLockSpecs lss) h = hPutStr h $ show lss
+putRecordh (RCServerInfo sinfo) h = hPutStr h $ show sinfo
+
+modifyRecord :: Record -> (RecordContents -> RecordContents) -> DBM ()
+modifyRecord rec f = do
+    h <- recordPath rec >>= liftIO . flip openFile ReadWriteMode 
+    liftIO $ do
+	Just rc <- getRecordh rec h
+	hSeek h AbsoluteSeek 0
+	putRecordh (f rc) h
+	hTell h >>= hSetFileSize h 
+	hClose h
+
+delRecord :: Record -> DBM ()
+delRecord rec = recordPath rec >>= liftIO . removeFile
+
+newLockRecord :: Lock -> DBM LockSpec
+newLockRecord lock = do
+    dbpath <- ask
+    let path = dbpath++[pathSeparator]++"lastlock"
+    h <- liftIO $ openFile path ReadWriteMode
+    contents <- liftIO $ hGetStrict h
+    let ls = if null contents then 0 else 1 + read contents
+    liftIO $ hSeek h AbsoluteSeek 0
+    liftIO $ hPutStr h $ show ls
+    liftIO $ hClose h
+    putRecord (RecLock ls) (RCLock lock)
+    return ls
+
+listUsers :: DBM [Codename]
+listUsers = do
+    dbpath <- ask
+    liftIO $ (map unpathifyName . filter ((==3).length)) <$> getDirectoryContents (dbpath++[pathSeparator]++"users")
+
+recordPath :: Record -> DBM FilePath
+recordPath rec = do
+	dbpath <- ask
+	return $ dbpath ++ [pathSeparator] ++ recordPath' rec
+    where
+	recordPath' (RecPassword name) = userDir name ++ "passwd"
+	recordPath' (RecUserInfo name) = userDir name ++ "info"
+	recordPath' (RecUserInfoLog name) = userDir name ++ "log"
+	recordPath' (RecLock ls) = locksDir ++ show ls
+	recordPath' (RecNote (NoteInfo name _  alock)) = userDir name ++ "notes" ++ [pathSeparator] ++ alockFN alock
+	recordPath' (RecRetiredLocks name) = userDir name ++ "retired"
+	recordPath' RecLockHashes = "lockHashes"
+	recordPath' RecServerInfo = "serverInfo"
+
+	userDir name = "users" ++ [pathSeparator] ++ pathifyName name ++ [pathSeparator]
+	alockFN (ActiveLock name idx) = pathifyName name ++":"++ show idx
+	locksDir = "locks"++[pathSeparator]
+
+-- dummy out characters which are disallowed on unix or windows:
+pathifyName = map $ \c -> case c of 
+    '/'->'s'
+    '.'->'d'
+    '\\'->'b'
+    '<'->'l'
+    '>'->'g'
+    ':'->'c'
+    '|'->'p'
+    '?'->'q'
+    '*'->'a'
+    _ -> c
+unpathifyName = map $ \c -> case c of 
+    's'->'/'
+    'd'->'.'
+    'b'->'\\'
+    'l'->'<'
+    'g'->'>'
+    'c'->':'
+    'p'->'|'
+    'q'->'?'
+    'a'->'*'
+    _ -> c 
diff --git a/Debug.hs b/Debug.hs
new file mode 100644
--- /dev/null
+++ b/Debug.hs
@@ -0,0 +1,20 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module Debug where
+
+import qualified Debug.Trace as Trace
+import Text.Show.Pretty (ppShow)
+
+prettyTrace :: Show a => a -> b -> b
+prettyTrace = Trace.trace . ppShow
+prettyTraceVal :: Show a => a -> a
+prettyTraceVal x = prettyTrace x x
+
diff --git a/EditGameState.hs b/EditGameState.hs
new file mode 100644
--- /dev/null
+++ b/EditGameState.hs
@@ -0,0 +1,155 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module EditGameState (modTile, mergeTiles) where
+
+import Control.Applicative hiding ((<*>))
+import Data.Function (on)
+import qualified Data.Map as Map
+import Data.Map (Map)
+import Data.Maybe
+import Control.Monad
+import Data.List
+
+import Hex
+import GameState
+import GameStateTypes
+--import Debug
+	
+modTile :: Maybe Tile -> HexPos -> HexPos -> Bool -> GameState -> GameState
+modTile tile pos lastPos painting st =
+    let board = stateBoard st
+	curOwnedTile = Map.lookup pos board
+	(st',mowner) = case curOwnedTile of
+	    Nothing -> (st,Nothing)
+	    Just (owner,SpringTile _ _) -> (delConnectionsIn pos st, Just owner)
+	    Just (owner,_) -> delPiecePos owner pos st -- XXX may invalidate board's indices to st
+	board' = stateBoard st'
+	addPiece p = addpp $ PlacedPiece pos p
+	lastMOwner = do
+	    (o,_) <- Map.lookup lastPos board
+	    return o
+	{-
+	same = isJust $ do
+		t <- tile
+		(_,t') <- curOwnedTile
+		guard $ ((==) `on` tileType) t t'
+		return $ Just ()
+	-}
+	lastElem os = isJust $ do
+	       lastOwner <- lastMOwner
+	       guard $ lastOwner `elem` os
+	lastWasDiff = isNothing $ do
+	       lastOwner <- lastMOwner
+	       owner <- mowner
+	       guard $ owner == lastOwner
+	lastOK = painting || lastWasDiff
+	validSpringRootTile ot = case snd ot of
+	    BlockTile _ -> True
+	    PivotTile _ -> True
+	    _ -> False
+	-- |Find next adjacent, skipping over current entity.
+	-- XXX: Note this doesn't work when the entity is adjacent in multiple
+	-- non-contiguous directions.
+	nextOfAdjacents adjs loop = listToMaybe $ fromMaybe adjs $ do
+		owner <- mowner
+		i <- elemIndex owner adjs
+		return $ (dropWhile (== owner) $ drop i adjs) ++
+		    if loop && i > 0 then adjs else []
+    in case mowner of
+	 Just o | protectedPiece o -> st
+	 _ -> (case tile of
+	   -- _ | same && (pos /= lastPos) -> id
+	   Just (BlockTile _) ->
+	       let adjacentBlocks = [ idx |
+		       dir <- hexDirs
+		       , Just (idx, BlockTile _) <- [Map.lookup (dir <+> pos) board']
+		       , not $ protectedPiece idx ]
+		   addToIdx = if lastOK && lastElem adjacentBlocks
+		       then lastMOwner
+		       else nextOfAdjacents adjacentBlocks False
+	       in case addToIdx of
+		  Nothing -> addPiece $ Block [zero]
+		  Just b -> addBlockPos b pos
+	   Just (ArmTile armdir _) -> 
+	       let adjacentPivots = [ idx |
+		       dir <- if armdir == zero then hexDirs else [armdir, neg armdir]
+		       , Just (idx, PivotTile _) <- [Map.lookup (dir <+> pos) board'] ]
+		   addToIdx = if lastOK && lastElem adjacentPivots
+		       then lastMOwner
+		       else nextOfAdjacents adjacentPivots True
+	       in case addToIdx of
+		      Nothing -> id
+		      Just p -> addPivotArm p pos
+	   Just (SpringTile _ _) ->
+	       let possibleSprings = [ Connection root end $ Spring sdir natLen |
+		       sdir <- hexDirs
+		       , let epos = sdir <+> pos
+		       , Just (eidx, BlockTile _) <- [Map.lookup epos board']
+		       , not $ protectedPiece eidx
+		       , (ridx, rpos) <- maybeToList $ castRay (neg sdir <+> pos) (neg sdir) board'
+		       , Just True == (validSpringRootTile `liftM` Map.lookup rpos board')
+		       , let natLen = hexLen (rpos <-> epos) - 1
+		       , natLen > 0
+		       {-
+		       , null [ conn |
+			   conn@(Connection _ _ (Spring sdir' _)) <-
+			       springsAtIdx st' eidx ++ springsEndAtIdx st' ridx
+			   , not $ sdir' `elem` [sdir,neg sdir] ]
+		       -}
+		       , not $ connGraphPathExists st' eidx ridx
+		       , let end = (eidx, epos <-> (placedPos $ getpp st' eidx))
+		       , let root = (ridx, rpos <-> (placedPos $ getpp st' ridx))
+		       ]
+		   nextSpring = listToMaybe $ fromMaybe possibleSprings $ do
+		       (_,SpringTile _ _) <- curOwnedTile -- XXX: therefore the indices of st are still valid
+		       i <- findIndex (`elem` connections st) possibleSprings
+		       return $ drop (i+1) possibleSprings
+	       in case nextSpring of
+		      Nothing -> id
+		      Just conn -> addConn conn
+	   Just (PivotTile _) -> addPiece $ Pivot []
+	   Just (WrenchTile _) -> addPiece $ Wrench zero
+	   Just HookTile -> let arm = listToMaybe [ dir | 
+				    dir <- hexDirs
+				    , isNothing $ Map.lookup (dir <+> pos) board' ]
+			    in case arm of Just armdir -> addPiece $ Hook armdir NullHF
+					   _ -> id
+	   Just (BallTile) -> addPiece Ball
+	   _ -> id
+	   ) st'
+
+-- | merge tile/piece with a neighbouring piece. If we merge a piece with
+-- connections, the connections are deleted: otherwise we'd need some fiddly
+-- conditions to ensure connection graph acyclicity.
+mergeTiles :: HexPos -> HexDir -> Bool -> GameState -> GameState
+mergeTiles pos dir mergePiece st = fromMaybe st $ do
+    let board = stateBoard st
+    (idx,tile) <- Map.lookup pos board
+    (idx',tile') <- Map.lookup (dir<+>pos) board
+    guard $ idx /= idx'
+    guard $ all (not . protectedPiece) [idx,idx']
+    case tile of
+	BlockTile _ -> do
+	    BlockTile _ <- Just tile'
+	    let st' = if mergePiece
+			  then delPiece idx st
+			  else fst $ delPiecePos idx pos st
+	    (idx'',_) <- Map.lookup (dir<+>pos) $ stateBoard st'
+	    return $ if mergePiece
+			 then foldr (addBlockPos idx'') st'
+				$ plPieceFootprint $ getpp st idx
+			 else addBlockPos idx'' pos st'
+	ArmTile _ _  -> do
+	    PivotTile _ <- Just tile'
+	    let st' = fst $ delPiecePos idx pos st
+	    (idx'',_) <- Map.lookup (dir<+>pos) $ stateBoard st'
+	    return $ addPivotArm idx'' pos st'
+
diff --git a/Frame.hs b/Frame.hs
new file mode 100644
--- /dev/null
+++ b/Frame.hs
@@ -0,0 +1,84 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module Frame where
+
+import Data.List ((\\))
+import qualified Data.Vector as Vector
+
+import GameState
+import GameStateTypes
+import Hex
+
+data Frame = BasicFrame Int
+    deriving (Eq, Ord, Show, Read)
+
+frameSize :: Frame -> Int
+frameSize (BasicFrame size) = size
+ 
+bolthole, entrance :: Frame -> HexVec
+bolthole (BasicFrame size) = size<*>hu <+> (size`div`2)<*>hv
+entrance f = neg $ bolthole f
+
+boltWidth :: Frame -> Int
+boltWidth (BasicFrame size) = size`div`4+1
+
+
+baseState :: Frame -> GameState
+baseState f =
+    GameState 
+	(Vector.fromList $ [ framePiece f, bolt ] ++ (initTools f))
+	[]
+    where
+	bolt = PlacedPiece (bolthole f <+> origin) $ Block $
+	    [ n<*>hu | n <- [-1..boltWidth f - 1] ] -- ++ [(-2)<*>hu<+>neg hv]
+
+framePiece :: Frame -> PlacedPiece
+framePiece f@(BasicFrame size) =
+    PlacedPiece origin $ Block $
+	map (bolthole f <+>) (
+	    [ bw<*>hu <+> n<*>hv | n <- [0..bw] ]
+	    ++ [ bw<*>hu <+> i<*>hw <+> n<*>hv | i <- [1..bw-1], n <- [0,bw+i] ])
+	++ (map (entrance f <+>) [neg hu <+> hv, 2 <*> neg hu, neg hu <+> hw,
+		2 <*> hw, neg hv <+> hw])
+	++ (concat [
+		map (rotate r) [ (n<*>hu) <+> (size<*>hw) | n <- [0..size-1] ] | r <- [0..5] ] \\ 
+	    [bolthole f, entrance f])
+    where bw = boltWidth f
+
+initTools :: Frame -> [PlacedPiece]
+initTools f =
+    [ PlacedPiece (entrance f <+> neg hu <+> origin) $ Wrench zero,
+    PlacedPiece (entrance f <+> hw <+> origin) $ Hook (neg hw) NullHF ]
+
+clearToolArea :: Frame -> GameState -> GameState
+clearToolArea f st = foldr delPieceIn st [entrance f <+> v <+> origin
+	    | v <- [ neg hu, hw, zero ] ]
+
+boltArea :: Frame -> [HexPos]
+boltArea f = map PHS
+	[ bolthole f <+> bw<*>hu <+> i<*>hw <+> n<*>hv | i <- [1..bw-1], n <- [1..bw+i-1] ]
+    where bw = boltWidth f
+
+inBounds :: Frame -> HexPos -> Bool
+inBounds f pos = hexLen (pos <-> origin) < frameSize f
+inEditable :: Frame -> HexPos -> Bool
+inEditable f pos = inBounds f pos || pos `elem` boltArea f ++ [PHS $ bolthole f]
+checkBounds :: Frame -> HexPos -> HexPos -> HexPos
+checkBounds f def pos = if inBounds f pos then pos else def
+checkEditable :: Frame -> HexPos -> HexPos -> HexPos
+checkEditable f def pos = if inEditable f pos then pos else def
+truncateToBounds,truncateToEditable :: Frame -> HexPos -> HexPos
+truncateToBounds f pos@(PHS v) = PHS $ truncateToLength (frameSize f - 1) v
+truncateToEditable f pos@(PHS v) = if inBounds f pos
+    then pos
+    else let pos' = PHS $ truncateToLength (frameSize f + boltWidth f - 1) v
+	in if inEditable f pos'
+	    then pos' else truncateToBounds f pos'
diff --git a/GameState.hs b/GameState.hs
new file mode 100644
--- /dev/null
+++ b/GameState.hs
@@ -0,0 +1,352 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module GameState where
+
+import Control.Applicative hiding ((<*>))
+import Data.Function (on)
+import qualified Data.Map as Map
+import Data.Map (Map)
+import qualified Data.Set as Set
+import Data.Set (Set)
+import qualified Data.Vector as Vector
+import Data.Maybe
+import Control.Monad
+import Control.Monad.State
+import Data.List
+import Data.Vector (Vector, (!), (//))
+
+import Hex
+import Util
+import GameStateTypes
+--import Debug
+
+ppidxs :: GameState -> [PieceIdx]
+ppidxs = Vector.toList . (Vector.findIndices $ const True) . placedPieces
+
+getpp :: GameState -> PieceIdx -> PlacedPiece
+getpp st idx = (placedPieces st) ! idx
+
+setpp :: PieceIdx -> PlacedPiece -> GameState -> GameState
+setpp idx pp st@(GameState pps _) = 
+    let displacement = (placedPos $ getpp st idx) <-> placedPos pp
+	updateConn conn@(Connection root@(ridx,rpos) end@(eidx,epos) link)
+	    | ridx == idx = Connection (ridx,rpos<+>displacement) end link
+	    | eidx == idx = Connection root (eidx,epos<+>displacement) link
+	    | otherwise = conn
+    in st {placedPieces = pps // [(idx, pp)]
+	, connections = map updateConn $ connections st }
+
+addpp :: PlacedPiece -> GameState -> GameState
+addpp pp st@(GameState pps _) = st {placedPieces = Vector.snoc pps pp}
+
+addConn :: Connection -> GameState -> GameState
+addConn conn st@(GameState _ conns) = st {connections = conn:conns}
+
+type Component = (HexVec, Set HexVec)
+components :: Set HexVec -> [Component]
+components patt
+    | Set.null patt = []
+    | otherwise =
+	let c = if zero `Set.member` patt then zero else head $ Set.toList patt
+	    (patt',comp) = floodfill c patt
+	in ( (c, Set.map (<+> neg c) comp) : components patt' )
+
+floodfill :: HexVec -> Set HexVec -> (Set HexVec, Set HexVec)
+floodfill start patt = floodfill' start `execState` (patt, Set.empty)
+    where 
+	floodfill' :: HexVec -> State (Set HexVec, Set HexVec) ()
+	floodfill' start = do
+	      (patt, dels) <- get
+	      let patt' = Set.delete start patt
+	      unless (Set.size patt' == Set.size patt) $ do
+		  put (patt', Set.insert start dels)
+		  sequence_ [ floodfill' (dir<+>start) | dir <- hexDirs ]
+
+delPiece :: PieceIdx -> GameState -> GameState
+delPiece idx (GameState pps conns) =
+    GameState (Vector.concat [Vector.take idx pps, Vector.drop (idx+1) pps])
+	[ Connection (ridx',rv) (eidx',ev) link |
+	    Connection (ridx,rv) (eidx,ev) link <- conns
+	    , ridx /= idx
+	    , eidx /= idx
+	    , let ridx' = if ridx > idx then ridx-1 else ridx
+	    , let eidx' = if eidx > idx then eidx-1 else eidx ]
+
+delPieceIn :: HexPos -> GameState -> GameState
+delPieceIn pos st =
+    case liftM fst $ Map.lookup pos $ stateBoard st of
+	Just idx -> delPiece idx st
+	_ -> st
+
+setPiece :: PieceIdx -> Piece -> GameState -> GameState
+setPiece idx p st = 
+    setpp idx (PlacedPiece (placedPos $ getpp st idx) p) st
+
+adjustPieces :: (Piece -> Piece) -> GameState -> GameState
+adjustPieces f st =
+    st { placedPieces = fmap
+	(\pp -> pp { placedPiece = f $ placedPiece pp })
+       $ placedPieces st }
+
+addBlockPos :: PieceIdx -> HexPos -> GameState -> GameState
+addBlockPos b pos st =
+    let PlacedPiece ppos (Block patt) = getpp st b
+    in setPiece b (Block (pos <-> ppos:patt)) st
+
+addPivotArm :: PieceIdx -> HexPos -> GameState -> GameState
+addPivotArm p pos st =
+    let PlacedPiece ppos (Pivot arms) = getpp st p
+    in setPiece p (Pivot (pos <-> ppos:arms)) st
+
+locusPos :: GameState -> Locus -> HexPos
+locusPos s (idx,v) = v <+> (placedPos $ getpp s idx)
+
+posLocus :: GameState -> HexPos -> Maybe Locus
+posLocus st pos = listToMaybe [ (idx,pos<->ppos) |
+    (idx,pp@(PlacedPiece ppos _)) <- enumVec $ placedPieces st
+    , pos `elem` plPieceFootprint pp ]
+
+connectionLength :: GameState -> Connection -> Int
+connectionLength st (Connection root end _) =
+    let rootPos = locusPos st root
+	endPos = locusPos st end
+    in hexLen (endPos <-> rootPos) - 1
+
+springsAtIdx,springsEndAtIdx,springsRootAtIdx :: GameState -> PieceIdx -> [Connection]
+springsAtIdx st idx =
+    [ c | c@(Connection (ridx,_) (eidx, _) (Spring _ _)) <- connections st
+	  , idx `elem` [ridx,eidx] ]
+springsAtIdxIgnoring st idx idx' =
+    [ c | c@(Connection (ridx,_) (eidx, _) (Spring _ _)) <- connections st
+	  , idx `elem` [ridx,eidx], idx' `notElem` [ridx,eidx] ]
+springsEndAtIdx st idx =
+    [ c | c@(Connection _ (eidx, _) (Spring _ _)) <- connections st
+	  , eidx==idx ]
+springsRootAtIdx st idx =
+    [ c | c@(Connection (ridx, _) _ (Spring _ _)) <- connections st
+	  , ridx==idx ]
+connectionsBetween :: GameState -> PieceIdx -> PieceIdx -> [Connection]
+connectionsBetween st idx idx' =
+    filter connIsBetween $ connections st
+    where
+	connIsBetween conn =
+	    isPerm (idx,idx') (fst $ connectionRoot conn, fst $ connectionEnd conn)
+	isPerm (x,y) (x',y') = (x==x'&&y==y')||(x==y'&&y==x')
+
+connGraphPathExists :: GameState -> PieceIdx -> PieceIdx -> Bool
+connGraphPathExists st ridx eidx = (ridx == eidx) ||
+	any ((connGraphPathExists st `flip` eidx) . fst .  connectionEnd)
+	    (springsRootAtIdx st ridx)
+
+connGraphHeight :: GameState -> PieceIdx -> Int
+connGraphHeight st idx =
+    maximum (0 : map ((+1) . connGraphHeight st . fst . connectionRoot) (springsEndAtIdx st idx))
+
+type DiGraph a = Map a (Set a)
+checkConnGraphAcyclic :: GameState -> Bool
+checkConnGraphAcyclic st =
+    let idxs = ppidxs st
+	leaves dg = map fst $ filter (Set.null . snd) $ Map.toList dg
+	checkDiGraphAcyclic dg = case listToMaybe $ leaves dg of
+	    Nothing -> Map.null dg
+	    Just leaf -> checkDiGraphAcyclic $ Map.delete leaf $ fmap (Set.delete leaf) dg
+    in checkDiGraphAcyclic $ Map.fromList 
+	[ (idx, Set.fromList $ map (fst.connectionRoot) $ springsEndAtIdx st idx) | idx <- idxs ]
+
+repossessConns :: GameState -> GameState -> GameState
+repossessConns st st' =
+    st' {connections = [ Connection root' end' link |
+	Connection root end link <- connections st
+	, root' <- maybeToList $ posLocus st' $ locusPos st root
+	, end' <- maybeToList $ posLocus st' $ locusPos st end ] }
+
+delConnectionsIn :: HexPos -> GameState -> GameState
+delConnectionsIn pos st =
+    st {connections = filter
+	((pos `notElem`) . connectionFootPrint st)
+        $ connections st}
+
+delPiecePos :: PieceIdx -> HexPos -> GameState -> (GameState, Maybe PieceIdx)
+-- ^ returns new state and the new index of what remains of the piece, if
+-- anything
+delPiecePos idx pos st =
+    let PlacedPiece ppos p = getpp st idx
+	v = pos <-> ppos
+    in case p of
+        Block patt ->
+	    let (st',midx) = componentify idx $ setpp idx (PlacedPiece ppos $ Block $ patt \\ [v]) st
+	    in (repossessConns st st', midx)
+        Pivot arms -> if v == zero
+	   then (delPiece idx st, Nothing)
+	   else ((setPiece idx $ Pivot $ arms \\ [v]) st, Just idx)
+        _ -> (delPiece idx st, Nothing)
+componentify :: PieceIdx -> GameState -> (GameState, Maybe PieceIdx)
+componentify idx st = let PlacedPiece ppos p = getpp st idx
+    in case p of
+        Block patt ->
+	    let comps = components $ Set.fromList patt
+		ppOfComp (v,patt) = PlacedPiece (v<+>ppos) $ Block $ Set.toList patt
+	    in case comps of
+		[] -> (delPiece idx st, Nothing)
+		zeroComp:newComps ->
+		    (setpp idx (ppOfComp zeroComp)
+			$ foldr (addpp . ppOfComp) st newComps, Just idx)
+	_ -> (st,Nothing)
+
+springExtended,springCompressed,springFullyExtended,springFullyCompressed :: GameState -> Connection -> Bool
+springExtended st c@(Connection _ _ (Spring _ natLen)) = connectionLength st c > natLen
+springExtended _ _ = False
+springCompressed st c@(Connection _ _ (Spring _ natLen)) = connectionLength st c < natLen
+springCompressed _ _ = False
+springFullyExtended st c@(Connection _ _ (Spring _ natLen)) = connectionLength st c >= 2*natLen
+springFullyExtended _ _ = False
+springFullyCompressed st c@(Connection _ _ (Spring _ natLen)) = connectionLength st c <= (natLen+1)`div`2
+springFullyCompressed _ _ = False
+springExtensionValid st c@(Connection _ _ (Spring _ natLen)) = let l = connectionLength st c in 
+    l >= (natLen+1)`div`2 && l <= 2*natLen
+springExtensionValid _ _ = True
+
+stateBoard :: GameState -> GameBoard
+stateBoard st@(GameState plPieces conns) =
+    addConnAdjs st conns $
+	(Map.unions $ map plPieceBoard $ enumVec plPieces) `Map.union`
+	(Map.unions $ map (connectionBoard st) conns)
+
+addConnAdjs :: GameState -> [Connection] -> GameBoard -> GameBoard
+addConnAdjs st = flip $ foldr addConnAdj
+    where
+	addConnAdj (Connection root end (Spring dir _)) board =
+	    addAdj (locusPos st root) dir $
+	    addAdj (locusPos st end) (neg dir) board
+	addConnAdj _ board = board
+	addAdj pos d =
+	    Map.adjust (\(o,tile) -> (o,case tile of
+		    BlockTile adjs -> BlockTile (d:adjs)
+		    _ -> tile))
+		pos
+
+plPieceBoard :: (PieceIdx,PlacedPiece) -> GameBoard
+plPieceBoard (idx,pp) = fmap (\x -> (idx,x)) $ plPieceMap pp
+
+plPieceMap :: PlacedPiece -> Map HexPos Tile
+plPieceMap (PlacedPiece pos (Block pattern)) =
+    Map.fromList [ (rel <+> pos, BlockTile adjs) |
+	rel <- pattern
+	, let adjs = filter (\dir -> rel <+> dir `elem` pattern) hexDirs ]
+plPieceMap (PlacedPiece pos (Pivot arms)) =
+    let overarmed = length arms > 2 in
+    Map.fromList $ (pos, PivotTile $ if overarmed then (head arms) else zero ) : 
+	[ (rel <+> pos, ArmTile rel main) |
+	    (rel,main) <- zip arms $ repeat False -- overarmed:$repeat False
+	    ]
+plPieceMap (PlacedPiece pos (Hook arm _)) =
+    Map.fromList $ (pos, HookTile) : [ (arm <+> pos, ArmTile arm True) ]
+plPieceMap (PlacedPiece pos (Wrench mom)) = Map.singleton pos $ WrenchTile mom
+plPieceMap (PlacedPiece pos Ball) = Map.singleton pos BallTile
+
+plPieceFootprint :: PlacedPiece -> [HexPos]
+plPieceFootprint = Map.keys . plPieceMap
+
+fullFootprint :: GameState -> PieceIdx -> [HexPos]
+-- ^footprint of piece and connections ending at it
+fullFootprint st idx = plPieceFootprint (getpp st idx) ++
+    (concat $ map (connectionFootPrint st) $ springsEndAtIdx st idx)
+
+footprintAt :: GameState -> PieceIdx -> [HexPos]
+-- ^footprint of piece and any connections at it
+footprintAt st idx = plPieceFootprint (getpp st idx) ++
+    (concat $ map (connectionFootPrint st) $ springsAtIdx st idx)
+
+footprintAtIgnoring :: GameState -> PieceIdx -> PieceIdx -> [HexPos]
+-- ^footprint of piece and any connections at it, except those with idx'
+footprintAtIgnoring st idx idx' = plPieceFootprint (getpp st idx) ++
+    (concat $ map (connectionFootPrint st) $ springsAtIdxIgnoring st idx idx')
+
+collisions :: GameState -> PieceIdx -> PieceIdx -> [HexPos]
+-- ^intersections of two pieces and their connections, disregarding
+-- the connections which connect the two pieces
+collisions st idx idx' =
+    intersect (footprintAt st idx) (footprintAt st idx') \\
+	(concat $ map (connectionFootPrint st) $ connectionsBetween st idx idx')
+
+connectionBoard :: GameState -> Connection -> GameBoard
+connectionBoard st (Connection root end@(eidx,_) (Spring dir natLen)) =
+    let rootPos = locusPos st root
+	endPos = locusPos st end
+	curLen = hexLen (endPos <-> rootPos) - 1
+    in Map.fromList $
+	[ ((d <*> dir) <+> rootPos, (eidx, SpringTile extension dir))
+	    | d <- [1..curLen],
+	    let extension | d <= natLen - curLen = Compressed
+			  | curLen-d < 2*(curLen - natLen) = Stretched
+			  | otherwise = Relaxed ]
+connectionBoard _ _ = Map.empty
+
+connectionFootPrint :: GameState -> Connection -> [HexPos]
+connectionFootPrint s c = Map.keys $ connectionBoard s c
+
+castRay :: HexPos -> HexDir -> GameBoard -> Maybe (PieceIdx, HexPos)
+castRay start dir board =
+    castRay' 30 start
+    where castRay' 0 _ = Nothing
+	  castRay' n pos =
+	      case Map.lookup pos board of
+		  Nothing -> castRay' (n-1) (dir<+>pos)
+		  Just (idx,_) -> Just (idx,pos)
+
+validGameState :: GameState -> Bool
+validGameState st@(GameState pps conns) = and
+    [ checkValidHex st
+    , checkConnGraphAcyclic st
+    , and [ null $ collisions st idx idx'
+	    | idx <- ppidxs st
+	    , idx' <- [0..idx-1] ]
+    , and [ isHexDir dir
+	    && castRay (dir<+>rpos) dir
+		(stateBoard $ GameState pps (conns \\ [c]))
+		== Just (eidx, epos)
+	    && springExtensionValid st c
+	    && (validRoot st root)
+	    && (validEnd st end)
+	    | c@(Connection root@(ridx,_) end@(eidx,_) (Spring dir _)) <- conns
+	    , let [rpos,epos] = map (locusPos st) [root,end] ]
+    , and [ 1 == length (components $ Set.fromList patt) 
+	    | Block patt <- map placedPiece $ Vector.toList pps ]
+    ]
+
+validRoot st (idx,v) = case placedPiece $ getpp st idx of
+    (Block _) -> True
+    (Pivot _) -> v==zero
+    _ -> False
+validEnd st (idx,_) = case placedPiece $ getpp st idx of
+    (Block _) -> True
+    _ -> False
+
+checkValidHex (GameState pps conns) = and
+    [ all validPP $ Vector.toList pps
+    , all validConn conns ]
+    where
+	validVec (HexVec x y z) = x+y+z==0
+	validPos (PHS v) = validVec v
+	validDir v = validVec v && isHexDir v
+	validPP (PlacedPiece pos piece) = validPos pos && validPiece piece
+	validPiece (Block patt) = all validVec patt
+	validPiece (Pivot arms) = all validDir arms
+	validPiece (Hook dir _) = validDir dir
+	validPiece _ = True
+	validConn (Connection (_,rv) (_,ev) link) = all validVec [rv,ev] && validLink link
+	validLink (Free v) = validVec v
+	validLink (Spring dir _) = validDir dir
+
+protectedPiece :: PieceIdx -> Bool
+protectedPiece = isFrame
+isFrame :: PieceIdx -> Bool
+isFrame = (==0)
diff --git a/GameStateTypes.hs b/GameStateTypes.hs
new file mode 100644
--- /dev/null
+++ b/GameStateTypes.hs
@@ -0,0 +1,84 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module GameStateTypes where
+
+import Data.Map (Map)
+import Data.Vector (Vector)
+import Hex
+
+data GameState = GameState { placedPieces :: Vector PlacedPiece,
+	connections :: [Connection] }
+    deriving (Eq, Ord, Show, Read)
+
+data PlacedPiece = PlacedPiece { placedPos :: HexPos, placedPiece :: Piece }
+    deriving (Eq, Ord, Show, Read)
+
+type PieceIdx = Int
+
+data Piece = Block { blockPattern :: [HexVec] }
+	    | Pivot { pivotArms :: [HexDir] }
+	    | Hook { hookArm :: HexDir, hookForce :: HookForce}
+	    | Wrench { wrenchMomentum :: HexDir }
+	    | Ball
+    deriving (Eq, Ord, Show, Read)
+
+data HookForce = NullHF | PushHF HexDir | TorqueHF Int
+    deriving (Eq, Ord, Show, Read)
+
+isBlock, isPivot, isHook, isWrench, isTool, isBall :: Piece -> Bool
+isBlock p = case p of Block _ -> True; _ -> False
+isPivot p = case p of Pivot _ -> True; _ -> False
+isHook p = case p of Hook _ _ -> True; _ -> False
+isWrench p = case p of Wrench _ -> True; _ -> False
+isTool p = isWrench p || isHook p
+isBall p = case p of Ball -> True; _ -> False
+
+data Connection = Connection { connectionRoot :: Locus
+		    , connectionEnd :: Locus, connectionLink :: Link }
+    deriving (Eq, Ord, Show, Read)
+
+type Locus = (PieceIdx, HexVec)
+
+data Link = Free { freePos :: HexVec }
+	  | Spring { springDir :: HexDir, springNatLength :: Int }
+    deriving (Eq, Ord, Show, Read)
+
+data SpringExtension = Relaxed | Compressed | Stretched
+    deriving (Eq, Ord, Show, Read)
+
+data Tile = BlockTile [HexDir] | PivotTile HexDir | ArmTile HexDir Bool | HookTile | WrenchTile HexDir
+	  | BallTile | SpringTile SpringExtension HexDir
+    deriving (Eq, Ord, Show, Read)
+
+tileType :: Tile -> Tile
+tileType (BlockTile _) = BlockTile []
+tileType (PivotTile _) = PivotTile zero
+tileType (ArmTile _ _) = ArmTile zero False
+tileType (WrenchTile _) = WrenchTile zero
+tileType (BallTile) = BallTile
+tileType (SpringTile _ _) = SpringTile Relaxed zero
+tileType t = t
+
+type OwnedTile = (PieceIdx, Tile)
+type GameBoard = Map HexPos OwnedTile
+
+-- |TorqueDir: Int of absolute value <= 1
+type TorqueDir = Int
+
+-- |'force' encompasses both usual directional forces and torques; we use
+-- 'push' for the former.
+data Force = Push PieceIdx HexDir | Torque PieceIdx TorqueDir
+    deriving (Eq, Ord, Show)
+
+-- |Alert: for passing information about physics processing to the UI
+data Alert = AlertCollision HexPos | AlertBlockingForce Force
+	   | AlertResistedForce Force | AlertBlockedForce Force
+    deriving (Eq, Ord, Show)
diff --git a/GraphColouring.hs b/GraphColouring.hs
new file mode 100644
--- /dev/null
+++ b/GraphColouring.hs
@@ -0,0 +1,92 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module GraphColouring where
+
+import qualified Data.Map as Map
+import Data.Map (Map)
+import qualified Data.Set as Set
+import Data.Set (Set)
+import qualified Data.Vector as Vector
+import Data.List
+import Data.Maybe
+
+type Colouring a = Map a Int
+type Graph a = (Set a, Set (Set a))
+type PlanarGraph a = Map a [a]
+
+fourColour :: Ord a => Graph a -> Colouring a -> Colouring a
+fourColour (nodes,edges) lastCol =
+    -- bruteforce 
+    if Map.keysSet lastCol == nodes && isColouring lastCol
+	then lastCol
+	else head $ filter isColouring colourings
+    where 
+	isColouring mapping = and [
+	    Map.lookup s mapping /= Map.lookup e mapping |
+		edge <- Set.toList edges
+		, [s,e] <- [Set.toList edge] ]
+	colourings = colourings' $ Set.toList nodes
+	colourings' [] = [ Map.empty ]
+	colourings' (n:ns) = [ Map.insert n c m |
+	    m <- colourings' ns
+	    , c <- [0..3] ]
+
+fiveColour :: Ord a => PlanarGraph a -> Colouring a -> Colouring a
+-- ^algorithm based on that presented in
+-- http://people.math.gatech.edu/~thomas/PAP/fcstoc.pdf 
+-- Key point: a planar graph can't have all vertices of degree >= 6
+-- (Proof: suppose it does, so |E| >= 3|V|; WLOG the graph is triangulated,
+-- so then |F| <= 2/3 |E|. So \xi = |V|-|E|+|F| <= (1/3 - 1 + 2/3)|E| = 0.
+-- But a planar graph has Euler characteristic 1.)
+fiveColour g lastCol =
+    if Map.keysSet lastCol == Map.keysSet g && isColouring lastCol
+	then lastCol
+	else fiveColour' g
+    where
+    isColouring mapping = and [
+	Map.lookup s mapping /= Map.lookup e mapping |
+	    s <- Map.keys g
+	    , e <- g Map.! s ]
+    --fiveColour' :: PlanarGraph a -> Colouring a
+    fiveColour' g
+	| g == Map.empty = Map.empty
+	| otherwise =
+	let 
+	    adjsOf v = (nub $ g Map.! v) \\ [v]
+	    v = head $ filter ((<=5) . length . adjsOf) $ Map.keys g
+	    adjs = adjsOf v
+	    addTo c = let vc = head $ possCols v \\ map (c Map.!) adjs
+		      in Map.insert v vc c
+	in if length adjs < 5
+	   then addTo $ fiveColour' $ deleteNode v g
+	   else let (v',v'') = if adjs!!2 `elem` (g Map.! (adjs!!0))
+			then (adjs!!1,adjs!!3)
+			else (adjs!!0,adjs!!2)
+		in addTo $ demerge v' v'' $ fiveColour' $ merge v v' v'' g
+    --possCols :: a -> [Int]
+    possCols v = maybe [0..4] (\lvc -> lvc:([0..4] \\ [lvc])) $ Map.lookup v lastCol
+    --demerge :: a -> a -> Colouring a -> Colouring a
+    demerge v v' c = Map.insert v' (c Map.! v) c
+    --merge :: a -> a -> a -> PlanarGraph a -> PlanarGraph a
+    merge v v' v'' g =
+	deleteNode v $ contractNodes v' v''
+	    $ Map.adjust (concatAdjsOver v $ g Map.! v'') v' g
+    --concatAdjsOver :: a -> [a] -> [a] -> [a]
+    concatAdjsOver v adjs adjs' =
+	let (s,_:e) = splitAt (fromJust $ elemIndex v adjs) adjs
+	in s ++ adjs' ++ e
+    deleteNode v =
+	fmap (filter (/= v)) . Map.delete v
+    contractNodes v v' =
+	fmap (map (\v'' -> if v'' == v' then v else v'')) . Map.delete v'
+    
+
+
diff --git a/Hex.lhs b/Hex.lhs
new file mode 100644
--- /dev/null
+++ b/Hex.lhs
@@ -0,0 +1,244 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+\begin{document}
+
+An abstract hex board type.
+
+We coordinatize by the integral points of the hyperplane x+y+z=0:
+
+Some hopefully elucidatory diagrams:
+
+   .   .
+   v.        u = (1,0,-1)
+ .   .___.   v = (-1,1,0)
+   w,  u     w = (0,-1,1)
+   .   .
+                                   X
+                             -2-1 0
+       Y                     , , , 1
+     . | .               2 -. . * , 2
+       |                1 -. . * * ,     * : "principal hextant"
+   .   .   .         Y 0 -. . 0 . .           x>=0&&y>0
+     /   \             -1 -. . . . `
+   / .   . \            -2 -. . . `-2
+  Z          X               ` ` `-1
+                              2 1 0
+                                 Z
+
+
+\begin{code}
+{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, FlexibleContexts #-}
+module Hex where
+
+import Data.Ix
+import Data.Monoid
+import Data.Ratio
+import Data.List (minimumBy)
+import Data.Function (on)
+   
+data HexVec = HexVec {hx,hy,hz :: Int} deriving (Eq, Ord, Show, Read)
+
+hu,hv,hw :: HexVec
+hu = HexVec    1    0 (-1)
+hv = HexVec (-1)    1    0
+hw = HexVec    0 (-1)    1
+
+hv2tup :: HexVec -> (Int,Int,Int)
+hv2tup (HexVec x y z) = (x,y,z)
+
+tup2hv :: (Int,Int,Int) -> HexVec
+tup2hv (x,y,z) 
+    | x+y+z == 0 = HexVec x y z
+    | otherwise = error "bad hex"
+
+hv2tupxy :: HexVec -> (Int,Int)
+hv2tupxy (HexVec x y _) = (x,y)
+
+tupxy2hv :: (Int,Int) -> HexVec
+tupxy2hv (x,y) = HexVec x y (-(x+y))
+
+hexLen :: HexVec -> Int
+hexLen (HexVec x y z) = maximum $ map abs [x,y,z]
+
+hexDot :: HexVec -> HexVec -> Int
+hexDot (HexVec x y z) (HexVec x' y' z') = x*x'+y*y'+z*z'
+
+hexDisc :: Int -> [HexVec] 
+hexDisc r = [ HexVec x y z | x <- [-r..r], y <- [-r..r],
+	let z = -x-y, abs z <= r ]
+
+hextant :: HexVec -> Int
+-- ^undefined at zero
+--	` 1 '
+--	2` '0
+--	--*--
+--	3' `5
+--	' 4 `
+hextant (HexVec x y z)
+    | x > 0 && y >= 0 = 0
+    | -z > 0 && -x >= 0 = 1
+    | y > 0 && z >= 0 = 2
+    | -x > 0 && -y >= 0 = 3
+    | z > 0 && x >= 0 = 4
+    | -y > 0 && -z >= 0 = 5
+    | otherwise = error $ "Tried to take hextant of zero"
+
+-- hextant (rotate n hu) == n
+rotate :: Int -> HexVec -> HexVec
+rotate 0 v = v
+rotate 2 (HexVec x y z) = HexVec z x y
+rotate (-2) (HexVec x y z) = HexVec y z x
+rotate 1 v = neg $ rotate (-2) v
+rotate (-1) v = neg $ rotate 2 v
+rotate n v | n < 0 = rotate (n+6) v
+	| n > 6 = rotate (n-6) v
+	| otherwise = rotate (n-2) (rotate 2 v)
+
+cmpAngles :: HexVec -> HexVec -> Ordering
+-- ^ordered by angle, taking cut along u
+cmpAngles v@(HexVec x y _) v'@(HexVec x' y' _)
+    | v == zero && v' == zero = EQ
+    | v == zero = LT
+    | compare (hextant v) (hextant v') /= EQ = 
+	compare (hextant v) (hextant v')
+    | hextant v /= 0 =
+	cmpAngles (rotate (-(hextant v)) v) (rotate (-(hextant v)) v')
+    | otherwise = compare (y%x) (y'%x')
+
+instance Ix HexVec where
+    range (h,h') = 
+	[ tupxy2hv (x,y) | (x,y) <- range (hv2tupxy h, hv2tupxy h') ]
+    inRange (h,h') h'' =
+	inRange (hv2tupxy h, hv2tupxy h') (hv2tupxy h'')
+    index (h,h') h'' =
+	index (hv2tupxy h , hv2tupxy h') (hv2tupxy h'')
+
+-- HexDirs are intended to be HexVecs of length <= 1
+type HexDir = HexVec
+isHexDir :: HexVec -> Bool
+isHexDir v = hexLen v == 1
+
+type HexDirOrZero = HexVec
+isHexDirOrZero :: HexVec -> Bool
+isHexDirOrZero v = hexLen v <= 1
+
+hexDirs :: [HexDir]
+hexDirs = map (`rotate` hu) [0..5]
+
+hexVec2HexDirOrZero :: HexVec -> HexDirOrZero
+hexVec2HexDirOrZero v
+    | v == zero = zero
+    | otherwise = rotate (hextant v) hu
+
+--minusHu = HexVec    (-1) 1    0
+--minusHv = HexVec    0    (-1) 1
+--minusHw = HexVec    1    0    (-1)
+
+canonDir :: HexDir -> HexDir
+canonDir dir | dir `elem` [ hu, hv, hw ] = dir
+	     | isHexDir dir = canonDir $ neg dir
+	     | dir == zero = zero
+	     | otherwise = undefined
+
+scaleToLength :: Int -> HexVec -> HexVec
+scaleToLength n v@(HexVec x y z) =
+    let
+	l = hexLen v
+	lv' = map ((`div`l).(n*)) [x,y,z]
+	minI = fst $ minimumBy (compare `on` snd) $
+	    zip [0..] $ map abs lv'
+	[x'',y'',z''] = zipWith (-) lv' [ d
+		| i <- [0..2]
+		, let d = if i == minI then sum lv' else 0 ]
+    in HexVec x'' y'' z''
+truncateToLength :: Int -> HexVec -> HexVec
+truncateToLength n v = if hexLen v <= n then v else scaleToLength n v
+\end{code}
+     
+Some general stuff on groups and actions and principal homogeneous spaces. We
+use additive notation, even though there's no assumption of commutativity.
+
+\begin{code}
+class Monoid g => Grp g where
+    neg :: g -> g
+    zero :: g
+    zero = mempty
+
+instance (Grp g1, Grp g2) => Grp (g1,g2) where
+    neg (a,b) = (neg a, neg b)
+
+infixl 6 <+>
+infixl 6 <->
+class Action a b where
+    (<+>) :: a -> b -> b
+instance Monoid m => Action m m where
+    (<+>) = mappend
+
+class Differable a b c where
+    (<->) :: a -> b -> c
+instance Grp g => Differable g g g where
+    x <-> y = x <+> (neg y)
+
+newtype PHS g = PHS { getPHS :: g }
+    deriving (Eq, Ord, Show, Read)
+
+instance Grp g => Action g (PHS g) where
+    x <+> (PHS y) = PHS (x <+> y)
+instance Grp g => Differable (PHS g) (PHS g) g where
+    (PHS x) <-> (PHS y) = x <-> y
+    
+infixl 7 <*>
+class MultAction a b where
+    (<*>) :: a -> b -> b
+    
+instance (Grp a, Integral n) => MultAction n a where
+    0 <*> _              = zero
+    1 <*> x              = x
+    n <*> x
+	| n < 0     = (-n) <*> (neg x)
+	| even n    = (n `div` 2) <*> (x <+> x)
+	| otherwise = x <+> ((n `div` 2) <*> (x <+> x))
+
+\end{code}
+
+Now we define HexSpaces as spaces acted on by HexVec, and with a canonical
+HexVec difference between two points (e.g. PHS HexVec).
+
+\begin{code}
+
+instance Monoid HexVec where
+    (HexVec x y z) `mappend` (HexVec x' y' z') = HexVec (x+x') (y+y') (z+z')
+    mempty = HexVec 0 0 0
+instance Grp HexVec where
+    neg (HexVec x y z) = HexVec (-x) (-y) (-z)
+
+class (Action HexVec b, Differable b b HexVec) => HexSpace b
+instance HexSpace (PHS HexVec)
+
+type HexPos = PHS HexVec
+origin :: HexPos
+origin = PHS zero
+
+\end{code}
+
+Testing:
+
+\begin{code}
+{-
+r = range (tup2hv (-3,-3,6), tup2hv (3,3,-6))
+test1 = index (tup2hv (-3,-3,6), tup2hv (3,3,-6)) (r!!5) == 5
+
+a :: PHS HexVec
+a = PHS zero
+test2 = hu <+> a
+-}
+\end{code}
+\end{document}
diff --git a/Init.hs b/Init.hs
new file mode 100644
--- /dev/null
+++ b/Init.hs
@@ -0,0 +1,82 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module Init where
+
+
+import Control.Applicative
+import Control.Monad
+import Control.Monad.Trans
+import Control.Monad.Trans.Maybe
+import Control.Monad.Trans.State
+import Data.Maybe
+import System.Exit
+import System.Environment
+import System.Directory
+import System.Console.GetOpt
+
+import Mundanities
+import AsciiLock
+import Lock
+import MainState
+import Interact
+
+data Opt = LockSize Int | ForceCurses
+    deriving (Eq, Ord, Show)
+options =
+    [ Option ['c'] ["curses"] (NoArg ForceCurses) "force curses UI"
+    -- , Option ['s'] ["locksize"] (ReqArg (LockSize . read) "SIZE") "locksize"
+    ]
+parseArgs :: [String] -> IO ([Opt],[String])
+parseArgs argv =
+    case getOpt Permute options argv of
+	(o,n,[]) -> return (o,n)
+	(_,_,errs) -> ioError (userError (concat errs ++ usageInfo header options))
+    where header = "Usage: intricacy [OPTION...] [file]"
+
+setup :: IO (Maybe Lock,[Opt],Maybe String)
+setup = do
+    argv <- getArgs
+    (opts,args) <- parseArgs argv
+    --let frameSize = fromMaybe 8 $ listToMaybe [ size | LockSize size <- opts ]
+    (fromJust <$>) $ runMaybeT $ msum [ do
+	rpath <- MaybeT $ return $ listToMaybe args
+	msum [ do
+	    path <- MaybeT $ (Just <$> canonicalizePath rpath)
+			`catchIO` (const $ return Nothing)
+	    lock <- reframe.fst <$> MaybeT (readLock path)
+	    return (Just lock,opts,Just path)
+	    , do
+		lift $ putStrLn $ "Failed to open lock file: "++rpath
+		lift $ exitFailure
+		mzero ]
+	, return (Nothing,opts,Nothing) ]
+
+main' :: (UIMonad s, UIMonad c) =>
+	Maybe (s MainState -> IO (Maybe MainState)) ->
+	Maybe (c MainState -> IO (Maybe MainState)) -> IO ()
+main' msdlUI mcursesUI = do
+    (mlock,opts,mpath) <- setup
+    initMState <- case mlock of
+	    Nothing -> initMetaState
+	    Just lock -> return $ newEditState lock Nothing mpath
+    void $ runMaybeT $ msum [ do
+	    finalState <- msum 
+		[ do
+		    guard $ ForceCurses `notElem` opts
+		    sdlUI <- MaybeT . return $ msdlUI
+		    MaybeT $ sdlUI $ interactUI `execStateT` initMState
+		, do
+		    cursesUI <- MaybeT . return $ mcursesUI
+		    MaybeT $ cursesUI $ interactUI `execStateT` initMState
+		]
+	    when (isNothing mlock) $ lift $ writeMetaState finalState
+	    lift $ exitSuccess
+	, lift exitFailure ]
diff --git a/InputMode.hs b/InputMode.hs
new file mode 100644
--- /dev/null
+++ b/InputMode.hs
@@ -0,0 +1,14 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module InputMode where
+
+data InputMode = IMEdit | IMPlay | IMReplay | IMMeta | IMTextInput
+    deriving (Eq, Ord, Show, Read)
diff --git a/Interact.hs b/Interact.hs
new file mode 100644
--- /dev/null
+++ b/Interact.hs
@@ -0,0 +1,623 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module Interact (interactUI) where 
+
+import Control.Monad.State
+import Control.Applicative
+import qualified Data.Vector as Vector
+import qualified Data.Map as Map
+import Data.Map (Map)
+import Control.Monad.Writer
+import Control.Monad.Trans.Error
+import Control.Monad.Trans.Maybe
+import Data.Maybe
+import Data.Char
+import Data.List
+import Control.Concurrent.STM
+import Control.Concurrent
+import System.Directory
+import System.FilePath
+import Data.Array
+import Data.Function (on)
+
+import Hex
+import Command
+import Physics
+import Mundanities
+import AsciiLock
+import GameState
+import GameStateTypes
+import EditGameState
+import Frame
+import Lock
+import Cache
+import Database
+import Protocol
+import Metagame
+import ServerAddr
+import MainState
+import InputMode
+import Maxlocksize
+import InteractUtil
+
+interactUI :: UIMonad uiM => StateT MainState uiM InteractSuccess
+interactUI = do
+    lift $ drawMessage ""
+    (IMMeta==) . ms2im <$> get >>= flip when (void $ do
+	flag <- gets newAsync
+	unblock <- lift unblockInput
+	_ <- liftIO $ forkIO $ forever $ do
+	    atomically $ readTVar flag >>= check >> writeTVar flag False
+	    unblock
+
+	-- draw before testing auth, lest a timeout mean a blank screen
+	drawMainState
+
+	testAuth
+	refreshUInfoUI
+	(isNothing <$> gets curAuth >>=) $ flip when $
+	    lift $ drawMessage "Welcome. To play the tutorial levels, press 'T'."
+	)
+    setMark '0'
+    loop
+
+    where
+	loop = do
+	    mainSt <- get
+	    let im = ms2im mainSt
+	    when (im == IMPlay && not (psSolved mainSt)) checkWon
+	    when (im == IMMeta) $ (checkAsyncErrors >>) $ void.runMaybeT $ 
+		mourNameSelected >>=
+		    flip when (lift purgeInvalidUndecls)
+	    drawMainState
+	    cmds <- lift $ getSomeInput im
+	    runErrorT (mapM_ (processCommand im) cmds) >>= \x -> case x of
+		Left ret -> do
+		    lift $ drawMessage ""
+		    return ret
+		Right _ -> loop
+
+-- | TODO: neater would be to use a stream (see package 'pipes') for
+-- input?
+getSomeInput im = do
+    cmds <- getInput im
+    if null cmds then getSomeInput im else return cmds
+
+newtype InteractSuccess = InteractSuccess Bool
+instance Error InteractSuccess where
+    noMsg = InteractSuccess False
+
+processCommand :: UIMonad uiM => InputMode -> Command -> ErrorT InteractSuccess (StateT MainState uiM) ()
+processCommand im CmdQuit = do
+    case im of
+	IMReplay -> throwError $ InteractSuccess False
+	IMPlay -> lift (gets psIsSub) >>= flip when (throwError $ InteractSuccess False)
+	IMEdit -> lift editStateUnsaved >>= flip unless (throwError $ InteractSuccess True)
+	_ -> return ()
+    title <- lift $ getTitle
+    really <- lift $ lift $ confirm $ "Really quit" ++ maybe "" (" from "++) title ++ "?"
+    when really $ throwError $ InteractSuccess False
+processCommand im CmdForceQuit = throwError $ InteractSuccess False
+processCommand IMPlay CmdOpen = do
+    st <- gets psCurrentState
+    frame <- gets psFrame
+    if checkSolved (frame,st) then throwError $ InteractSuccess True else lift.lift $ drawError "Locked!"
+processCommand im cmd = lift $ processCommand' im cmd
+
+processCommand' :: UIMonad uiM => InputMode -> Command -> StateT MainState uiM ()
+processCommand' im CmdHelp = lift $ showHelp im
+processCommand' im CmdBind = lift $ flip (>>) (drawMessage "") $ void $ runMaybeT $ do
+	lift $ drawMessage "Command to bind: "
+	cmd <- msum $ repeat $ do
+	    cmd <- MaybeT $ listToMaybe <$> getInput im
+	    guard $ not.null $ describeCommand cmd
+	    return cmd
+	lift $ drawMessage ("key to bind to \"" ++ describeCommand cmd ++ "\" (repeat binding to delete): ")
+	ch <- MaybeT getChRaw
+	guard $ ch /= '\ESC'
+	lift $ setUIBinding im cmd ch
+
+processCommand' _ CmdSuspend = lift $ suspend
+processCommand' _ CmdRedraw = lift $ redraw
+processCommand' im CmdMark = void.runMaybeT $ do
+    guard $ im `elem` [IMEdit, IMPlay, IMReplay]
+    str <- MaybeT $ lift $ textInput "Mark: " 1 False True Nothing Nothing
+    ch <- MaybeT $ return $ listToMaybe str
+    guard $ ch `notElem` ['0', '\'']
+    lift $ setMark ch
+processCommand' im CmdJumpMark = void.runMaybeT $ do
+    guard $ im `elem` [IMEdit, IMPlay, IMReplay]
+    str <- MaybeT $ lift $ textInput
+	"Jump to mark ('0' for starting state): " 1 False True Nothing Nothing
+    ch <- MaybeT $ return $ listToMaybe str
+    lift $ jumpMark ch
+processCommand' IMMeta (CmdSelCodename mname) = void.runMaybeT $ do
+    name <- msum [ MaybeT $ return $ mname
+	, do
+	    newCodename <- (map toUpper <$>) $ MaybeT $ lift $ textInput "Codename:" 3 False True Nothing Nothing
+	    guard $ length newCodename == 3
+	    return newCodename
+	]
+    guard $ validCodeName name
+    lift $ do
+	modify $ \ms -> ms { codenameStack = (name:codenameStack ms) }
+	invalidateUInfo name
+	refreshUInfoUI
+processCommand' IMMeta CmdHome = void.runMaybeT $ do
+    ourName <- mgetOurName
+    lift $ do
+	modify $ \ms -> ms { codenameStack = (ourName:codenameStack ms) }
+	refreshUInfoUI
+processCommand' IMMeta CmdBackCodename = do
+    stack <- gets codenameStack
+    when (length stack > 1) $ do
+	modify $ \ms -> ms { codenameStack = tail stack }
+	refreshUInfoUI
+processCommand' IMMeta CmdSetServer = void.runMaybeT $ do
+    saddr <- gets curServer
+    saddrs <- liftIO $ knownServers
+    newSaddrstr <- MaybeT $ lift $ textInput "Set server:" 256 False False
+	    (Just $ map saddrStr saddrs) (Just $ saddrStr saddr)
+    newSaddr <- MaybeT $ return $ strToSaddr newSaddrstr
+    modify $ \ms -> ms { curServer = newSaddr }
+    unless (nullSaddr newSaddr) $
+	msum [ void $ MaybeT $ getFreshRecBlocking RecServerInfo
+	, modify $ \ms -> ms { curServer = saddr }
+	]
+processCommand' IMMeta CmdToggleCacheOnly =
+    not <$> gets cacheOnly >>= \c -> modify $ \ms -> ms {cacheOnly = c}
+
+processCommand' IMMeta CmdRegister = void.runMaybeT $ do
+    newName <- mgetCurName
+    mauth <- gets curAuth
+    let isUs = maybe False ((==newName).authUser) mauth
+    confirmOrBail $ if isUs then "Really reset password?" else "Register codename "++newName++"?"
+    passwd <- (hashPassword newName <$>) $ MaybeT $ lift $
+	textInput "Enter new password:" 64 True False Nothing Nothing
+    lift $ if isUs then do
+	    resp <- curServerAction $ ResetPassword passwd
+	    case resp of
+		ServerAck -> do
+		    lift $ drawMessage "New password set."
+		    modify $ \ms -> ms {curAuth = Just $ Auth newName passwd}
+		ServerError err -> lift $ drawError err
+		_ -> lift $ drawMessage $ "Bad server response: " ++ show resp
+	else do
+	    modify $ \ms -> ms {curAuth = Just $ Auth newName passwd}
+	    resp <- curServerAction Register
+	    case resp of
+		ServerAck -> do
+		    invalidateUInfo newName
+		    refreshUInfoUI
+		    lift $ drawMessage "Registered!"
+		ServerError err -> do
+		    lift $ drawError err
+		    modify $ \ms -> ms {curAuth = Nothing}
+		_ -> lift $ drawMessage $ "Bad server response: " ++ show resp
+processCommand' IMMeta CmdAuth = void.runMaybeT $ do
+    auth <- lift $ gets curAuth
+    if isJust auth then do
+	confirmOrBail "Log out?"
+	modify $ \ms -> ms {curAuth = Nothing}
+    else do
+	name <- mgetCurName
+	passwd <- (hashPassword name <$>) $ MaybeT $ lift $
+	    textInput ("Enter password for "++name++":") 64 True False Nothing Nothing
+	lift $ do
+	    modify $ \ms -> ms {curAuth = Just $ Auth name passwd}
+	    resp <- curServerAction $ Authenticate
+	    case resp of
+		ServerAck -> (lift $ drawMessage "Authenticated.")
+		ServerMessage msg -> (lift $ drawMessage $ "Server: " ++ msg)
+		ServerError err -> do
+		    lift $ drawError err
+		    modify $ \ms -> ms {curAuth = auth}
+		_ -> lift $ drawMessage $ "Bad server response: " ++ show resp
+processCommand' IMMeta (CmdSolve midx) = void.runMaybeT $ do
+    name <- mgetCurName
+    uinfo <- mgetUInfo name
+    idx <- msum [ MaybeT $ return midx
+	, askLockIndex "Solve which lock?" "No lock to solve!" (\i -> isJust $ userLocks uinfo ! i) ]
+    ls <- MaybeT $ return $ lockSpec <$> userLocks uinfo ! idx
+    undecls <- lift (gets undeclareds)
+    msum [ do
+	    undecl <- MaybeT $ return $ find (\(Undeclared _ ls' _) -> ls == ls') undecls
+	    _ <- MaybeT $ gets curAuth
+	    confirmOrBail "Declare existing solution?"
+	    void.lift.runMaybeT $ -- ignores MaybeT failures
+		declare undecl
+	, do
+	    RCLock lock <- MaybeT $ getFreshRecBlocking $ RecLock ls
+	    soln <- solveLock lock $ Just $
+		"solving " ++ name ++ ":" ++ [lockIndexChar idx] ++ "  (#" ++ show ls ++")"
+	    mourName <- lift $ (authUser <$>) <$> gets curAuth
+	    guard $ mourName /= Just name
+	    let undecl = Undeclared soln ls (ActiveLock name idx) 
+	    msum [ do
+		    _ <- MaybeT $ gets curAuth
+		    confirmOrBail "Declare solution?"
+		    declare undecl
+		, unless (any (\(Undeclared _ ls' _) -> ls == ls') undecls) $
+		    modify $ \ms -> ms { undeclareds = (undecl : undeclareds ms) }
+		]
+	]
+processCommand' IMMeta (CmdPlayLockSpec mls) = void.runMaybeT $ do
+    ls <- msum [ MaybeT $ return mls
+	, do 
+	    tls <- MaybeT . lift $ textInput "Lock number:" 16 False False Nothing Nothing
+	    MaybeT . return $ fst <$> (listToMaybe $ (reads :: ReadS Int) tls)
+	]
+    RCLock lock <- MaybeT $ getFreshRecBlocking $ RecLock ls
+    solveLock lock $ Just $ "solving " ++ show ls
+    
+processCommand' IMMeta (CmdDeclare mundecl) = void.runMaybeT $ do
+    mourNameSelected >>= guard
+    name <- mgetCurName
+    undecls <- lift $ gets undeclareds
+    guard $ not $ null undecls
+    declare =<< msum [ MaybeT $ return mundecl
+	, if length undecls == 1
+	then return $ head undecls
+	else do
+	    which <- MaybeT $ lift $ textInput
+		("Declare which solution?")
+		5 False True Nothing Nothing
+	    MaybeT $ return $ msum
+		[ do
+		    i <- fst <$> (listToMaybe $ (reads :: ReadS Int) which)
+		    guard $ 0 < i && i <= length undecls
+		    return $ undecls !! (i-1)
+		, listToMaybe $ [ undecl
+		    | undecl@(Undeclared _ _ (ActiveLock name' i)) <- undecls
+		    , or [ take (length which) (name' ++ ":" ++ [lockIndexChar i]) ==
+			    map toUpper which 
+			, name'==name && [lockIndexChar i] == which
+			] ]
+		]
+	]
+processCommand' IMMeta (CmdViewSolution mnote) = void.runMaybeT $ do
+    note <- msum [ MaybeT $ return mnote, do
+	ourName <- mgetOurName
+	name <- mgetCurName
+	uinfo <- mgetUInfo name
+	noteses <- lift $ sequence
+	    [ case mlockinfo of
+		Nothing -> return []
+		Just lockinfo -> (++lockSolutions lockinfo) <$> do
+			ns <- getNotesReadOn lockinfo 
+			return $ if length ns < 3 then [] else ns
+	    | mlockinfo <- elems $ userLocks uinfo ]
+	idx <- askLockIndex "View solution to which lock?" "No solutions to view" $ not.null.(noteses!!)
+	let notes = noteses!!idx
+	    authors = map noteAuthor notes
+	author <- if length notes == 1
+	    then return $ noteAuthor $ head notes 
+	    else (map toUpper <$>) $ MaybeT $ lift $
+		textInput ("View solution by which player? ["
+		    ++ intercalate "," (take 3 authors)
+		    ++ if length authors > 3 then ",...]" else "]")
+		3 False True (Just $ authors) Nothing
+	MaybeT $ return $ find ((==author).noteAuthor) notes
+	]
+    let ActiveLock name idx = noteOn note
+    uinfo <- mgetUInfo name
+    ls <- lockSpec <$> MaybeT (return $ userLocks uinfo ! idx)
+    RCLock lock <- MaybeT $ getFreshRecBlocking $ RecLock ls
+    RCSolution soln <- MaybeT $ getFreshRecBlocking $ RecNote note
+    lift.lift $ execStateT interactUI $ newReplayState (snd.reframe$lock) soln $ Just $
+	"viewing solution by " ++ noteAuthor note ++ " to " ++ name ++ [':',lockIndexChar idx]
+
+processCommand' IMMeta (CmdPlaceLock midx) = void.runMaybeT $ do
+    mourNameSelected >>= guard
+    ourName <- mgetOurName
+    (lock,msoln) <- MaybeT $ gets curLock
+    lockpath <- lift $ gets curLockPath
+    ourUInfo <- mgetUInfo ourName
+    idx <- msum [ MaybeT $ return midx
+	, askLockIndex ("Place " ++ show lockpath ++ " in which slot?") "bug" $ const True
+	]
+    when (isJust $ userLocks ourUInfo ! idx) $
+	confirmOrBail "Really retire existing lock?"
+    soln <- msum [ MaybeT $ return msoln,
+	solveLock lock $ Just $ "testing lock" ]
+    lift $ do 
+	curServerActionAsync $ SetLock lock idx soln
+	invalidateUInfo ourName
+	refreshUInfoUI
+
+processCommand' IMMeta CmdSelectLock = void.runMaybeT $ do
+    lockdir <- liftIO $ confFilePath "locks"
+    paths <- liftIO $ map (drop (length lockdir + 1)) <$> getDirContentsRec lockdir
+    path <- MaybeT $ lift $ textInput "Select lock:" 1024 False False (Just paths) Nothing
+    lift $ setLockPath path
+processCommand' IMMeta CmdNextLock =
+    gets curLockPath >>= liftIO . nextLock True >>= setLockPath
+processCommand' IMMeta CmdPrevLock =
+    gets curLockPath >>= liftIO . nextLock False >>= setLockPath
+processCommand' IMMeta CmdNextPage =
+    modify $ \ms -> ms { listOffset = listOffset ms + 1 }
+processCommand' IMMeta CmdPrevPage =
+    modify $ \ms -> ms { listOffset = max 0 $ listOffset ms - 1 }
+processCommand' IMMeta CmdEdit = void.runMaybeT $ do
+    (lock, msoln) <- msum [ MaybeT $ gets curLock
+	, do
+	    frame <- BasicFrame <$> msum [ do
+		    _ <- gets curServer
+		    RCServerInfo (ServerInfo size _) <- MaybeT $ getFreshRecBlocking RecServerInfo
+		    return size
+		, do
+		    sizet <- MaybeT $ lift $ textInput
+			("Lock size: [3-" ++ show maxlocksize ++ "]") 2 False False Nothing Nothing
+		    size <- MaybeT $ return $ fst <$> (listToMaybe $ (reads :: ReadS Int) sizet)
+		    guard $ 3 <= size && size <= maxlocksize
+		    return size
+		]
+	    return ((frame, baseState frame), Nothing)
+	]
+    path <- lift $ gets curLockPath
+    newPath <- MaybeT $ lift $ (esPath <$>) $ execStateT interactUI $ newEditState (reframe lock)
+	msoln (if null path then Nothing else Just path)
+    lift $ setLockPath newPath
+processCommand' IMMeta CmdTutorials = void.runMaybeT $ do
+    tutdir <- liftIO $ getDataPath "tutorial"
+    tuts <- liftIO $ (sort . map (takeWhile (/='.')) . filter (isSuffixOf ".lock")) <$>
+	getDirectoryContents tutdir `catchIO` (const $ return [])
+    when (null tuts) $ do
+	lift.lift $ drawError "No tutorial levels found"
+	mzero
+    s <- MaybeT $ lift $ textInput ("Play which tutorial level [1-"++show (length tuts)++"]")
+	    (length (show (length tuts))) False True Nothing Nothing
+    i <- MaybeT $ return $ fst <$> (listToMaybe $ (reads :: ReadS Int) s)
+    guard $ 1 <= i && i <= length tuts
+    let dotut i = do
+	let name = tuts !! (i-1)
+	let pref = tutdir ++ [pathSeparator] ++ name
+	(lock,_) <- MaybeT $ liftIO $ readLock (pref ++ ".lock")
+	text <- MaybeT $ liftIO $ listToMaybe <$> readStrings (pref ++ ".text")
+	_ <- solveLock lock $ Just $ "Tutorial " ++ show i ++ ": " ++ text
+	if i+1 <= length tuts then do
+		confirmOrBail $ "Tutorial level completed! Play next tutorial level (" ++ show (i+1) ++ ")?"
+		dotut $ i+1
+	    else lift $ do 
+		mauth <- gets curAuth
+		lift $ drawMessage $ if isNothing mauth
+		    then "Tutorial completed! To play on the server, pick a codename ('C') and register it ('R')."
+		    else "Tutorial completed!"
+    dotut i
+processCommand' IMMeta CmdShowRetired = void.runMaybeT $ do
+    name <- mgetCurName
+    newRL <- lift (gets retiredLocks) >>= \rl -> case rl of
+	Nothing -> do
+	    RCLockSpecs lss <- MaybeT $ getFreshRecBlocking $ RecRetiredLocks name
+	    return $ Just lss
+	Just _ -> return Nothing
+    lift $ modify $ \ms -> ms {retiredLocks = newRL}
+    
+processCommand' IMPlay CmdUndo = do
+    st <- gets psCurrentState
+    stack <- gets psGameStateMoveStack
+    ustms <- gets psUndoneStack
+    unless (null stack) $ do
+	let (st',pm) = head stack
+	modify $ \ps -> ps {psCurrentState=st', psGameStateMoveStack = tail stack,
+	    psLastAlerts = [], psUndoneStack = (st,pm):ustms}
+processCommand' IMPlay CmdRedo = do
+    ustms <- gets psUndoneStack
+    case ustms of 
+	[] -> return ()
+	ustm:ustms' -> do
+	    pushPState ustm
+	    modify $ \ps -> ps {psUndoneStack = ustms'}
+processCommand' IMPlay (CmdManipulateToolAt pos) = do
+    board <- stateBoard <$> gets psCurrentState
+    wsel <- gets wrenchSelected
+    void.runMaybeT $ msum $ [ do
+	    tile <- MaybeT $ return $ snd <$> Map.lookup pos board
+	    guard $ case tile of {WrenchTile _ -> True; HookTile -> True; _ -> False}
+	    lift $ processCommand' IMPlay $ CmdTile tile
+	] ++ [ do
+	    tile <- MaybeT $ return $ snd <$> Map.lookup (d<+>pos) board
+	    guard $ tileType tile == if wsel then WrenchTile zero else HookTile
+	    lift $ processCommand' IMPlay $ CmdDir WHSSelected $ neg d
+	    | d <- hexDirs ]
+processCommand' IMPlay (CmdDrag pos dir) = do
+    board <- stateBoard <$> gets psCurrentState
+    wsel <- gets wrenchSelected
+    void.runMaybeT $ do
+	tile <- MaybeT $ return $ snd <$> Map.lookup pos board
+	guard $ case tile of {WrenchTile _ -> True; HookTile -> True; _ -> False}
+	lift $ processCommand' IMPlay $ CmdDir WHSSelected $ dir
+	board' <- stateBoard <$> gets psCurrentState
+	msum [ do
+		tile' <- MaybeT $ return $ snd <$> Map.lookup pos' board'
+		guard $ tileType tile' == if wsel then WrenchTile zero else HookTile
+		lift.lift $ warpPointer $ pos'
+	    | pos' <- [dir<+>pos, pos] ]
+
+processCommand' IMPlay cmd = do
+    wsel <- gets wrenchSelected
+    st <- gets psCurrentState
+    let push whs dir 
+	    | whs == WHSWrench || (whs == WHSSelected && wsel) =
+		Just $ WrenchPush dir
+	    | otherwise = Just $ HookPush dir
+	torque dir = Just $ HookTorque dir
+	(wsel', pm) =
+	    case cmd of
+		CmdTile (WrenchTile _) -> (True, Nothing)
+		CmdTile HookTile -> (False, Nothing)
+		CmdTile (ArmTile _ _) -> (False, Nothing)
+		CmdToggle -> (not wsel, Nothing)
+		CmdDir whs dir -> (wsel, push whs dir)
+		CmdRotate dir -> (wsel, torque dir)
+		CmdWait -> (wsel, Just NullPM)
+		CmdSelect -> (wsel, Just NullPM)
+		_ -> (wsel, Nothing)
+    modify $ \ps -> ps {wrenchSelected = wsel'}
+    case pm of
+	 Nothing -> return ()
+	 Just pm' -> do
+	    (st',alerts) <- lift $ doPhysicsTick pm' st
+	    modify $ \ps -> ps {psLastAlerts = alerts}
+	    pushPState (st',pm')
+
+processCommand' IMReplay (CmdReplayBack 1) = void.runMaybeT $ do
+    (st',pm) <- MaybeT $ listToMaybe <$> gets rsGameStateMoveStack
+    lift $ modify $ \rs -> rs {rsCurrentState=st'
+	, rsGameStateMoveStack = tail $ rsGameStateMoveStack rs
+	, rsMoveStack = pm:rsMoveStack rs}
+processCommand' IMReplay (CmdReplayBack n) = replicateM_ n $
+    processCommand' IMReplay (CmdReplayBack 1)
+processCommand' IMReplay (CmdReplayForward 1) = void.runMaybeT $ do
+    pm <- MaybeT $ listToMaybe <$> gets rsMoveStack
+    lift $ do
+	st <- gets rsCurrentState
+	(st',_) <- lift $ doPhysicsTick pm st
+	modify $ \rs -> rs {rsCurrentState = st'
+	    , rsGameStateMoveStack = (st,pm):rsGameStateMoveStack rs
+	    , rsMoveStack = tail $ rsMoveStack rs}
+processCommand' IMReplay (CmdReplayForward n) = replicateM_ n $
+    processCommand' IMReplay (CmdReplayForward 1)
+processCommand' IMReplay CmdUndo = processCommand' IMReplay (CmdReplayBack 1)
+processCommand' IMReplay CmdRedo = processCommand' IMReplay (CmdReplayForward 1)
+
+processCommand' IMEdit CmdPlay = do
+    st <- gets $ head.esGameStateStack
+    frame <- gets esFrame
+    modify $ \es -> es {selectedPiece = Nothing}
+    subPlay (frame,st)
+processCommand' IMEdit CmdTest = do
+    frame <- gets esFrame
+    modifyEState (\st -> snd $ canonify (frame, st))
+    modify $ \es -> es {selectedPiece = Nothing}
+    mpath <- gets esPath
+    st <- gets $ head.esGameStateStack
+    void.runMaybeT $ do
+	soln <- solveLock (frame,st) $ Just $ "testing " ++ fromMaybe "[unnamed lock]" mpath
+	lift $ modify $ \es -> es { esTested = Just (st, soln) }
+processCommand' IMEdit CmdUndo = do
+    st:sts <- gets esGameStateStack
+    usts <- gets esUndoneStack
+    unless (null sts) $ modify $ \es -> es {esGameStateStack = sts, esUndoneStack = st:usts}
+processCommand' IMEdit CmdRedo = do
+    usts <- gets esUndoneStack
+    case usts of 
+	[] -> return ()
+	ust:usts' -> do
+	    pushEState ust
+	    modify $ \es -> es {esUndoneStack = usts'}
+processCommand' IMEdit CmdUnselect =
+    modify $ \es -> es {selectedPiece = Nothing}
+processCommand' IMEdit CmdSelect = do
+    selPiece <- gets selectedPiece
+    selPos <- gets selectedPos
+    st:_ <- gets esGameStateStack
+    let selPiece' =
+	    if isJust selPiece
+		then Nothing
+		else liftM fst $ Map.lookup selPos $ stateBoard st
+    modify $ \es -> es {selectedPiece = selPiece'}
+processCommand' IMEdit (CmdDir _ dir) = do
+    selPos <- gets selectedPos
+    selPiece <- gets selectedPiece
+    frame <- gets esFrame
+    case selPiece of
+	Nothing -> modify $ \es -> es {selectedPos = checkEditable frame selPos $ dir <+> selPos}
+	Just p -> doForce $ Push p dir
+processCommand' IMEdit (CmdMoveTo newPos) = do
+    frame <- gets esFrame
+    modify $ \es -> es {selectedPos = truncateToEditable frame newPos}
+processCommand' IMEdit (CmdDrag pos dir) = do
+    board <- stateBoard.head <$> gets esGameStateStack
+    void.runMaybeT $ do
+	selIdx <- MaybeT $ gets selectedPiece
+	idx <- MaybeT $ return $ fst <$> Map.lookup pos board
+	guard $ idx == selIdx
+	lift $ processCommand' IMEdit $ CmdDir WHSSelected $ dir
+	board' <- stateBoard.head <$> gets esGameStateStack
+	msum [ do
+		idx' <- MaybeT $ return $ fst <$> Map.lookup pos' board'
+		guard $ idx' == selIdx
+		lift.lift $ warpPointer $ pos'
+	    | pos' <- [dir<+>pos, pos] ]
+processCommand' IMEdit (CmdRotate dir) = do
+    selPiece <- gets selectedPiece
+    case selPiece of
+	Nothing -> return ()
+	Just p -> doForce $ Torque p dir
+processCommand' IMEdit (CmdTile tile) = do
+    selPos <- gets selectedPos
+    drawTile selPos (Just tile) False
+processCommand' IMEdit (CmdPaint tile) = do
+    selPos <- gets selectedPos
+    drawTile selPos tile True
+processCommand' IMEdit (CmdPaintFromTo tile from to) = do
+    frame <- gets esFrame
+    paintTilePath tile (truncateToEditable frame from) (truncateToEditable frame to)
+processCommand' IMEdit CmdMerge = do
+    selPos <- gets selectedPos
+    st:_ <- gets esGameStateStack
+    lift $ drawMessage "Merge in which direction?"
+    cmd <- lift $ head <$> getSomeInput IMEdit
+    lift $ drawMessage ""
+    case cmd of
+	CmdDir _ mergeDir -> do
+	    modifyEState $ mergeTiles selPos mergeDir True
+	    -- XXX: merging might invalidate selectedPiece
+	    modify $ \es -> es {selectedPiece = Nothing}
+	_ -> return ()
+processCommand' IMEdit CmdWait = do
+    selPos <- gets selectedPos
+    selPiece <- gets selectedPiece
+    st:_ <- gets esGameStateStack
+    case selPiece of
+	Nothing -> drawTile selPos Nothing False
+	Just _ -> do (st',_) <- lift $ doPhysicsTick NullPM st
+		     pushEState st'
+processCommand' IMEdit CmdDelete = do
+    selPiece <- gets selectedPiece
+    st:_ <- gets esGameStateStack
+    case selPiece of
+	Nothing -> return ()
+	Just p -> do modify $ \es -> es {selectedPiece = Nothing}
+		     modifyEState $ delPiece p
+processCommand' IMEdit CmdWriteState = void.runMaybeT $ do
+    path <- lift $ gets esPath
+    newPath <- MaybeT $ lift $ textInput "Save lock as:" 1024 False False Nothing path
+    guard $ not $ null newPath
+    fullPath <- liftIO $ fullLockPath newPath
+    (liftIO (doesFileExist fullPath `catchIO` const (return True)) >>=) $ flip when $
+	confirmOrBail $ "Really overwrite '"++fullPath++"'?"
+    lift $ do
+	st <- gets $ head.esGameStateStack
+	frame <- gets esFrame
+	msoln <- getCurTestSoln
+	merr <- liftIO $ ((writeAsciiLockFile fullPath msoln $ canonify (frame, st)) >> return Nothing)
+	    `catchIO` (return . Just . show)
+	modify $ \es -> es {lastSavedState = Just (st,isJust msoln)}
+	case merr of
+	    Nothing -> modify $ \es -> es {esPath = Just newPath}
+	    Just err -> lift $ drawError $ "Write failed: "++err
+processCommand' _ _ = return ()
+
+hashPassword :: String -> String -> String
+-- ^ salt password
+hashPassword name password = hash $ "IY" ++ name ++ password
+
+subPlay :: UIMonad uiM => Lock -> StateT MainState uiM ()
+subPlay lock =
+    pushEState =<< psCurrentState <$> (lift $ execStateT interactUI $ newPlayState lock Nothing True)
+
+solveLock :: UIMonad uiM => Lock -> Maybe String -> MaybeT (StateT MainState uiM) Solution
+solveLock lock title = do
+    (InteractSuccess solved, ps) <- lift.lift $ runStateT interactUI $ newPlayState (reframe lock) title False
+    guard $ solved
+    return $ reverse $ (map snd) $ psGameStateMoveStack ps
diff --git a/InteractUtil.hs b/InteractUtil.hs
new file mode 100644
--- /dev/null
+++ b/InteractUtil.hs
@@ -0,0 +1,225 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module InteractUtil where
+
+import Control.Monad.State
+import Control.Applicative
+import qualified Data.Vector as Vector
+import qualified Data.Map as Map
+import Data.Map (Map)
+import Control.Monad.Writer
+import Control.Monad.Trans.Error
+import Control.Monad.Trans.Maybe
+import Data.Maybe
+import Data.Char
+import Data.List
+import System.Directory
+import Data.Array
+import Data.Function (on)
+
+import Hex
+import Command
+import Physics
+import Mundanities
+import GameStateTypes
+import EditGameState
+import Frame
+import Lock
+import Protocol
+import Metagame
+import MainState
+import InputMode
+
+checkWon :: UIMonad uiM => StateT MainState uiM ()
+checkWon = do
+    st <- gets psCurrentState
+    frame <- gets psFrame
+    when (checkSolved (frame,st)) $ do
+	modify $ \ps -> ps {psSolved = True}
+	lift $ drawMessage "Unlocked!"
+	{-
+	pms <- (reverse.(map snd)) `liftM` gets psGameStateMoveStack
+	let wfname = fromMaybe "unnamed.lock" fname 
+	liftIO $ writeFile (wfname++".solution") $ show pms
+	liftIO $ B.writeFile (wfname++".solution.bin") $ Bin.encode $ pms
+	-}
+doForce force = do
+    st:_ <- gets esGameStateStack
+    let (st',alerts) = runWriter $ resolveForces
+	    (Vector.singleton (ForceChoice [force])) Vector.empty
+	    (\_ _->False) st
+    lift (drawAlerts alerts) >> pushEState st'
+drawTile pos tile painting = do
+    modify $ \es -> es {selectedPiece = Nothing}
+    lastMP <- gets lastModPos
+    modifyEState $ modTile tile pos lastMP painting
+    modify $ \es -> es {lastModPos = pos}
+paintTilePath tile from to = if from == to
+    then modify $ \es -> es {lastModPos = to}
+    else let from' = (hexVec2HexDirOrZero $ to<->from) <+> from
+	in drawTile from' tile True >> paintTilePath tile from' to
+
+pushEState :: UIMonad uiM => GameState -> StateT MainState uiM ()
+pushEState st = do
+    st':sts <- gets esGameStateStack
+    when (st' /= st) $ modify $ \es -> es {esGameStateStack = st:st':sts, esUndoneStack = []}
+pushPState :: UIMonad uiM => (GameState,PlayerMove) -> StateT MainState uiM ()
+pushPState (st,pm) = do
+    st' <- gets psCurrentState
+    stms <- gets psGameStateMoveStack
+    when (st' /= st) $ modify $ \ps -> ps {psCurrentState = st,
+	    psGameStateMoveStack = (st',pm):stms, psUndoneStack = []}
+modifyEState :: UIMonad uiM => (GameState -> GameState) -> StateT MainState uiM ()
+modifyEState f = do
+    st:_ <- gets esGameStateStack
+    pushEState $ f st
+
+doPhysicsTick :: UIMonad uiM => PlayerMove -> GameState -> uiM (GameState, [Alert])
+doPhysicsTick pm st =
+    let r@(st',alerts) = runWriter $ physicsTick pm st in
+    drawAlerts alerts >> return r
+
+nextLock :: Bool -> FilePath -> IO FilePath
+nextLock newer path = do
+    lockdir <- confFilePath "locks"
+    time <- (Just <$> (fullLockPath path >>= getModificationTime))
+	`catchIO` const (return Nothing)
+    paths <- getDirContentsRec lockdir
+    maybe path (drop (length lockdir + 1) . fst) . listToMaybe .
+	(if newer then id else reverse) . sortBy (compare `on` snd) .
+	filter (maybe (const True)
+	    (\x y -> (if newer then (<) else (>)) x (snd y)) time) <$>
+	(\p -> (,) p <$> getModificationTime p) `mapM` paths
+
+setLockPath path = do
+    lock <- liftIO $ fullLockPath path >>= readLock
+    modify $ \ms -> ms {curLockPath = path, curLock = lock}
+
+declare undecl@(Undeclared soln ls al) = do
+    ourName <- mgetOurName
+    ourUInfo <- mgetUInfo ourName
+    idx <- askLockIndex "Secure behind which lock?"
+	"You first need to place a lock to secure your solution behind."
+	(\i -> isJust $ userLocks ourUInfo ! i)
+    guard $ isJust $ userLocks ourUInfo ! idx
+    lift $ do
+	curServerActionAsync $ DeclareSolution soln ls al idx
+	mapM_ invalidateUInfo [lockOwner al, ourName]
+	refreshUInfoUI
+
+jumpMark :: UIMonad uiM => Char -> StateT MainState uiM ()
+jumpMark ch = do
+    mst <- get
+    void.runMaybeT $ case ms2im mst of
+	IMEdit -> do
+	    st <- MaybeT $ return $ ch `Map.lookup` esMarks mst
+	    lift $ setMark '\'' >> pushEState st
+	IMPlay -> do
+	    mst' <- MaybeT $ return $ ch `Map.lookup` psMarks mst
+	    put mst' { psMarks = Map.insert '\'' mst $ psMarks mst }
+	IMReplay -> do
+	    mst' <- MaybeT $ return $ ch `Map.lookup` rsMarks mst
+	    put mst' { rsMarks = Map.insert '\'' mst $ rsMarks mst }
+	_ -> return ()
+
+setMark :: (Monad m) => Char -> StateT MainState m ()
+setMark ch = get >>= \mst -> case mst of
+    -- ugh... remind me why I'm not using lens?
+    EditState { esMarks = marks, esGameStateStack = (st:_) } ->
+	put $ mst { esMarks = Map.insert ch st marks }
+    PlayState {} -> put $ mst { psMarks = Map.insert ch mst $ psMarks mst }
+    ReplayState {} -> put $ mst { rsMarks = Map.insert ch mst $ rsMarks mst }
+    _ -> return ()
+
+askLockIndex prompt failMessage pred = do
+    let ok = filter pred [0,1,2]
+    case length ok of
+	0 -> (lift.lift) (drawError failMessage) >> mzero
+	1 -> return $ head ok
+	_ -> ask ok
+    where
+	ask ok = do
+	    let prompt' = prompt ++ " [" ++ intersperse ',' (map lockIndexChar ok) ++ "]"
+	    idx <- MaybeT $ lift $ join . (((charLockIndex<$>).listToMaybe)<$>) <$>
+		textInput prompt' 1 False True Nothing Nothing
+	    if idx `elem` ok then return idx else ask ok
+confirmOrBail prompt = (guard =<<) $ lift.lift $ confirm prompt
+confirm :: UIMonad uiM => String -> uiM Bool
+confirm prompt = do
+    drawMessage $ prompt ++ " [y/N]"
+    waitConfirm <* drawMessage ""
+    where
+	waitConfirm = do
+	    cmds <- getInput IMTextInput
+	    case msum $ map ansOfCmd cmds of
+		Just answer -> return answer
+		Nothing -> waitConfirm
+	ansOfCmd (CmdInputChar 'y') = Just True
+	ansOfCmd (CmdInputChar 'Y') = Just True
+	ansOfCmd CmdRedraw = Nothing
+	ansOfCmd CmdRefresh = Nothing
+	ansOfCmd _ = Just False
+
+-- | TODO: draw cursor
+textInput :: UIMonad uiM => String -> Int -> Bool -> Bool -> Maybe [String] -> Maybe String -> uiM (Maybe String)
+textInput prompt maxlen hidden endOnMax mposss init = getText (fromMaybe "" init, Nothing) <* drawMessage ""
+    where
+	getText :: UIMonad uiM => (String, Maybe String) -> uiM (Maybe String)
+	getText (s,mstem) = do
+	    drawMessage $ prompt ++ " " ++ if hidden then replicate (length s) '*' else s
+	    if endOnMax && isNothing mstem && maxlen <= length s
+	    then return $ Just $ take maxlen s
+	    else do
+		cmds <- getInput IMTextInput
+		case foldM applyCmd (s,mstem) cmds of
+		    Left False -> return Nothing
+		    Left True -> return $ Just s
+		    Right (s',mstem') -> getText (s',mstem')
+	    where
+		applyCmd (s,mstem) (CmdInputChar c) = case c of
+		    '\ESC' -> Left False
+		    '\a' -> Left False -- ^G
+		    '\ETX' -> Left False -- ^C
+		    '\n' -> Left True
+		    '\r' -> Left True
+		    '\NAK' -> Right ("",Nothing)  -- ^U
+		    '\b' -> Right $ (take (length s - 1) s, Nothing)
+		    '\DEL' -> Right $ (take (length s - 1) s, Nothing)
+		    '\t' -> case mposss of
+			Nothing -> Right (s,mstem)
+			Just possibilities -> case mstem of
+			    Nothing -> let
+				completions = filter (completes s) possibilities
+				pref = if null completions then s else
+				    let c = head completions
+				    in head [ c' | n <- reverse [0..length c],
+					let c'=take n c, all (completes c') completions ]
+				in Right (pref,Just pref)
+			    Just stem -> let
+				completions = filter (completes stem) possibilities
+				later = filter (>s) completions
+				s' | null completions = s
+				   | null later = head completions
+				   | otherwise = minimum later
+				in Right (s',mstem)
+		    _ -> Right $ if isPrint c
+			    then ((if length s >= maxlen then id else (++[c])) s, Nothing)
+			    else (s,mstem)
+		applyCmd x (CmdInputSelLock idx) =
+		    Right $ ([lockIndexChar idx], Nothing)
+		applyCmd x (CmdInputSelUndecl (Undeclared _ _ (ActiveLock name idx))) =
+		    Right $ (name++[':',lockIndexChar idx], Nothing)
+		applyCmd x (CmdInputCodename name) =
+		    Right $ (name, Nothing)
+		applyCmd x CmdRedraw = Right x
+		applyCmd x CmdRefresh = Right x
+		applyCmd _ _ = Left False
+	completes s s' = take (length s) s' == s
diff --git a/KeyBindings.hs b/KeyBindings.hs
new file mode 100644
--- /dev/null
+++ b/KeyBindings.hs
@@ -0,0 +1,199 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module KeyBindings (KeyBindings, bindings, findBindings, findBinding, showKey) where
+
+import Data.Maybe
+import Data.Char
+import Data.List
+
+import Command
+import Hex
+import GameStateTypes
+import InputMode
+
+type KeyBindings = [ (Char,Command) ]
+
+ctrl, unctrl :: Char -> Char
+ctrl c = toEnum $ fromEnum c - 64
+unctrl c = toEnum $ fromEnum c + 64
+
+lowerToo :: KeyBindings -> KeyBindings
+lowerToo = concat . map addLower
+    where addLower b@(c, cmd) = [ b, (toLower c, cmd) ]
+
+qwertyViHex =
+    [ ('l', CmdDir WHSHook hu)
+    , ('h', CmdDir WHSHook $ neg hu)
+    , ('b', CmdDir WHSHook hw)
+    , ('u', CmdDir WHSHook $ neg hw)
+    , ('y', CmdDir WHSHook hv)
+    , ('n', CmdDir WHSHook $ neg hv)
+    , ('L', CmdDir WHSWrench hu)
+    , ('H', CmdDir WHSWrench $ neg hu)
+    , ('B', CmdDir WHSWrench hw)
+    , ('U', CmdDir WHSWrench $ neg hw)
+    , ('Y', CmdDir WHSWrench hv)
+    , ('N', CmdDir WHSWrench $ neg hv)
+    , ('k', CmdRotate 1)
+    , ('j', CmdRotate $ -1)
+    ]
+qwertyLeftHex =
+    [ ('d', CmdDir WHSHook hu)
+    , ('a', CmdDir WHSHook $ neg hu)
+    , ('z', CmdDir WHSHook hw)
+    , ('e', CmdDir WHSHook $ neg hw)
+    , ('w', CmdDir WHSHook hv)
+    , ('x', CmdDir WHSHook $ neg hv)
+    , ('D', CmdDir WHSWrench hu)
+    , ('A', CmdDir WHSWrench $ neg hu)
+    , ('Z', CmdDir WHSWrench hw)
+    , ('E', CmdDir WHSWrench $ neg hw)
+    , ('W', CmdDir WHSWrench hv)
+    , ('X', CmdDir WHSWrench $ neg hv)
+    , ('q', CmdRotate 1)
+    , ('c', CmdRotate $ -1)
+    , ('S', CmdWait)
+    , ('s', CmdWait)
+    ]
+dvorakMidHex =
+    [ ('i', CmdDir WHSWrench hu)
+    , ('e', CmdDir WHSWrench $ neg hu)
+    , ('j', CmdDir WHSWrench hw)
+    , ('y', CmdDir WHSWrench $ neg hw)
+    , ('p', CmdDir WHSWrench hv)
+    , ('k', CmdDir WHSWrench $ neg hv)
+    ]
+
+keypadHex =
+    [ ('5', CmdWait)
+    , ('6', CmdDir WHSSelected hu)
+    , ('4', CmdDir WHSSelected $ neg hu)
+    , ('1', CmdDir WHSSelected hw)
+    , ('9', CmdDir WHSSelected $ neg hw)
+    , ('7', CmdDir WHSSelected hv)
+    , ('3', CmdDir WHSSelected $ neg hv)
+    , ('8', CmdRotate 1)
+    , ('2', CmdRotate $ -1)
+    ]
+
+miscLockGlobal = lowerToo
+    [ ('X', CmdUndo)
+    , ('\b', CmdUndo)
+    , ('R', CmdRedo)
+    , (ctrl 'R', CmdRedo)
+    , (ctrl 'U', CmdUndo)
+    , ('.', CmdWait)
+    , ('Z', CmdWait)
+    , ('M', CmdMark)
+    , ('\'', CmdJumpMark)
+    ]
+
+miscGlobal = lowerToo
+    [ ('Q', CmdQuit)
+    , (ctrl 'C', CmdQuit)
+    , ('?', CmdHelp)
+    , (ctrl 'B', CmdBind)
+    , (ctrl 'L', CmdRedraw)
+    , (ctrl 'Z', CmdSuspend)
+    ]
+
+lockGlobal = keypadHex ++ qwertyViHex ++ miscGlobal ++ miscLockGlobal
+
+playOnly = lowerToo
+    [ ('O', CmdOpen)
+    , (' ', CmdWait)
+    , ('\r', CmdWait)
+    , ('\n', CmdWait)
+    , ('\t', CmdToggle)
+    , ('*', CmdTile $ WrenchTile zero)
+    , ('/', CmdTile HookTile)
+    , ('@', CmdTile HookTile)
+    ]
+replayOnly =
+    [ (' ', CmdReplayForward 1)
+    ]
+editMisc = lowerToo
+    [ ('P', CmdPlay)
+    , (' ', CmdSelect)
+    , ('\r', CmdSelect)
+    , ('\n', CmdSelect)
+    , ('T', CmdTest)
+    , ('W', CmdWriteState)
+    , (ctrl 'S', CmdWriteState)
+    , ('&', CmdMerge)
+    , ('E', CmdDelete)
+    ]
+tilesPaintRow = lowerToo
+    [ ('G', CmdTile BallTile)
+    , ('F', CmdTile $ ArmTile zero False)
+    , ('D', CmdTile $ PivotTile zero)
+    , ('S', CmdTile $ SpringTile Relaxed zero)
+    , ('A', CmdTile $ BlockTile [])
+    , ('Z', CmdWait)
+    ]
+tilesAscii =
+    [ ('o', CmdTile $ PivotTile zero)
+    , ('O', CmdTile BallTile)
+    , ('S', CmdTile $ SpringTile Relaxed zero)
+    --, ('s', CmdTile $ SpringTile Stretched zero)
+    --, ('$', CmdTile $ SpringTile Compressed zero)
+    , ('-', CmdTile $ ArmTile zero False)
+    --, ('-', CmdTile $ ArmTile hu False)
+    , ('\\', CmdTile $ ArmTile hv False)
+    , ('/', CmdTile $ ArmTile hw False)
+    , ('@', CmdTile HookTile)
+    , ('*', CmdTile $ WrenchTile zero)
+    , ('#', CmdTile $ BlockTile [])
+    ]
+editOnly = editMisc ++ tilesPaintRow ++ tilesAscii
+
+playBindings = playOnly ++ lockGlobal
+replayBindings = replayOnly ++ lockGlobal
+editBindings = editOnly ++ lockGlobal
+metaBindings = lowerToo
+    [ ('C', CmdSelCodename Nothing)
+    , ('H', CmdHome)
+    , ('B', CmdBackCodename)
+    , ('S', CmdSolve Nothing)
+    , ('D', CmdDeclare Nothing)
+    , ('V', CmdViewSolution Nothing)
+    , ('R', CmdRegister)
+    , ('P', CmdPlaceLock Nothing)
+    , ('E', CmdEdit)
+    , ('L', CmdSelectLock)
+    , ('O', CmdPrevLock)
+    , ('N', CmdNextLock)
+    , ('A', CmdAuth)
+    , ('T', CmdTutorials)
+    , ('+', CmdShowRetired)
+    , ('#', CmdPlayLockSpec Nothing)
+    , ('$', CmdSetServer)
+    , ('^', CmdToggleCacheOnly)
+    , ('>', CmdNextPage)
+    , ('<', CmdPrevPage)
+    ] ++ miscGlobal
+
+bindings :: InputMode -> KeyBindings
+bindings IMEdit = editBindings
+bindings IMPlay = playBindings
+bindings IMMeta = metaBindings
+bindings IMReplay = replayBindings
+bindings _ = []
+
+findBindings :: KeyBindings -> Command -> [Char]
+findBindings bdgs cmd = nub [ ch | (ch,cmd') <- bdgs, cmd'==cmd ]
+
+findBinding :: KeyBindings -> Command -> Maybe Char
+findBinding = (listToMaybe.) . findBindings
+
+showKey ch | isPrint ch = [ch]
+showKey ch | isPrint (unctrl ch) = ('^':[unctrl ch])
+showKey _ = "[?]"
diff --git a/Lock.hs b/Lock.hs
new file mode 100644
--- /dev/null
+++ b/Lock.hs
@@ -0,0 +1,90 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module Lock where
+import Control.Monad.Writer
+import Data.Maybe
+import qualified Data.Map as Map
+
+import Frame
+import GameState
+import GameStateTypes
+import Hex
+import Util
+import Physics
+
+type Lock = (Frame, GameState)
+liftLock :: (GameState -> GameState) -> (Lock -> Lock)
+liftLock g (f,st) = (f,g st)
+
+lockSize (f,_) = frameSize f
+
+deframe :: Lock -> Lock
+deframe = delTools . liftLock (setpp 0 nullpp)
+nullpp = PlacedPiece (PHS zero) (Block [])
+
+reframe :: Lock -> Lock
+reframe l@(f, st) = addTools $ delTools $ liftLock (setpp 0 (framePiece f)) l
+
+validLock :: Lock -> Bool
+validLock lock@(f,st) = and
+    [ st == stepPhysics st
+    , lock == reframe lock
+    , validGameState st 
+    ]
+
+stepPhysics :: GameState -> GameState
+stepPhysics = fst.runWriter.physicsTick NullPM
+
+type Solution = [PlayerMove]
+
+checkSolution :: Lock -> Solution -> Bool
+checkSolution lock pms =
+    let (frame,st) = reframe lock
+    in any (\st' -> checkSolved (frame,st')) $
+		scanl (((fst.runWriter).).flip physicsTick) st pms
+
+checkSolved :: Lock -> Bool
+checkSolved (f,st) =
+    let b = stateBoard st in
+	and [ isNothing $ Map.lookup p b | p <- boltArea f ]
+
+
+canonify :: Lock -> Lock
+canonify = addTools . stabilise . delTools . delOOB
+delTools :: Lock -> Lock
+delTools = liftLock delTools'
+    where
+	delTools' :: GameState -> GameState
+	delTools' st =
+	    case listToMaybe [ idx | (idx,pp) <- enumVec $ placedPieces st
+			      , isTool $ placedPiece pp ] of
+		Nothing -> st
+		Just idx -> delTools' $ delPiece idx st
+addTools :: Lock -> Lock
+addTools (f,st) =
+    let st' = clearToolArea f st
+    in (f, foldr addpp st' $ initTools f)
+stabilise :: Lock -> Lock
+stabilise = liftLock stabilise'
+    where
+	stabilise' :: GameState -> GameState
+	stabilise' st =
+	    let st' = stepPhysics st
+	    in if st == st' then st else stabilise' st'
+
+delOOB :: Lock -> Lock
+delOOB l@(f,st) = case listToMaybe [ idx |
+	(idx,_) <- enumVec $ placedPieces st
+	, not $ isFrame idx
+	, all (not.inBounds f) $ fullFootprint st idx
+	, null $ springsEndAtIdx st idx] of
+    Nothing -> l
+    Just idx -> delOOB $ liftLock (delPiece idx) l
diff --git a/MainBoth.hs b/MainBoth.hs
new file mode 100644
--- /dev/null
+++ b/MainBoth.hs
@@ -0,0 +1,22 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module Main where
+
+import Init
+import qualified SDLUI (UIM)
+import SDLUIMInstance ()
+import qualified CursesUI (UIM)
+import CursesUIMInstance ()
+import MainState
+
+main = main' 
+    (Just (doUI::SDLUI.UIM MainState -> IO (Maybe MainState)))
+    (Just (doUI::CursesUI.UIM MainState -> IO (Maybe MainState)))
diff --git a/MainCurses.hs b/MainCurses.hs
new file mode 100644
--- /dev/null
+++ b/MainCurses.hs
@@ -0,0 +1,20 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module Main where
+
+import Init
+import qualified CursesUI (UIM)
+import CursesUIMInstance ()
+import MainState
+
+main = main' 
+    (Nothing :: (Maybe (CursesUI.UIM MainState -> IO (Maybe MainState))))
+    (Just (doUI::CursesUI.UIM MainState -> IO (Maybe MainState)))
diff --git a/MainSDL.hs b/MainSDL.hs
new file mode 100644
--- /dev/null
+++ b/MainSDL.hs
@@ -0,0 +1,20 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module Main where
+
+import Init
+import qualified SDLUI
+import SDLUIMInstance ()
+import MainState
+
+main = main' 
+    (Just (doUI::SDLUI.UIM MainState -> IO (Maybe MainState)))
+    (Nothing :: (Maybe (SDLUI.UIM MainState -> IO (Maybe MainState))))
diff --git a/MainState.hs b/MainState.hs
new file mode 100644
--- /dev/null
+++ b/MainState.hs
@@ -0,0 +1,370 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module MainState where
+
+import Control.Monad.State
+import Control.Applicative
+import qualified Data.Vector as Vector
+import qualified Data.Map as Map
+import Data.Map (Map)
+import Control.Monad.Writer
+import Control.Monad.Trans.Error
+import Control.Monad.Trans.Maybe
+import Data.Maybe
+import Data.Char
+import Data.List
+import Control.Concurrent.STM
+import Control.Concurrent
+import System.Directory
+import System.FilePath
+import Data.Time.Clock
+import Data.Array
+import Data.Function (on)
+
+import Hex
+import Mundanities
+import AsciiLock
+import GameStateTypes
+import Physics
+import Command
+import Frame
+import Lock
+import Cache
+import Database
+import Protocol
+import Metagame
+import ServerAddr
+import InputMode
+
+class (Applicative m, MonadIO m) => UIMonad m where
+    runUI :: m a -> IO a
+    initUI :: m Bool
+    endUI :: m ()
+    drawMainState :: StateT MainState m ()
+    drawAlerts :: [Alert] -> m ()
+    drawMessage :: String -> m ()
+    drawError :: String -> m ()
+    showHelp :: InputMode -> m ()
+    getInput :: InputMode -> m [ Command ]
+    getChRaw :: m ( Maybe Char )
+    unblockInput :: m (IO ())
+    setUIBinding :: InputMode -> Command -> Char -> m ()
+    getDrawImpatience :: m ( Int -> IO () )
+    warpPointer :: HexPos -> m ()
+    suspend,redraw :: m ()
+
+    doUI :: m a -> IO (Maybe a)
+    doUI m = runUI $ do
+	ok <- initUI
+	if ok then m >>= (endUI >>).return.Just else return Nothing
+
+-- | this could be neatened using GADTs
+data MainState 
+    = PlayState
+	{ psCurrentState::GameState
+	, psFrame::Frame
+	, psLastAlerts::[Alert]
+	, wrenchSelected::Bool
+	, psSolved::Bool
+	, psGameStateMoveStack::[(GameState, PlayerMove)]
+	, psUndoneStack::[(GameState, PlayerMove)]
+	, psTitle::Maybe String
+	, psIsSub::Bool
+	, psMarks::Map Char MainState
+	}
+    | ReplayState
+	{ rsCurrentState::GameState
+	, rsMoveStack::[PlayerMove]
+	, rsGameStateMoveStack::[(GameState, PlayerMove)]
+	, rsTitle::Maybe String
+	, rsMarks::Map Char MainState
+	}
+    | EditState
+	{ esGameStateStack::[GameState]
+	, esUndoneStack::[GameState]
+	, esFrame::Frame
+	, esPath::Maybe FilePath
+	, esTested::Maybe (GameState,Solution)
+	, lastSavedState::Maybe (GameState, Bool)
+	, selectedPiece::Maybe PieceIdx
+	, selectedPos::HexPos
+	, lastModPos::HexPos
+	, esMarks::Map Char GameState
+	}
+    | MetaState
+	{ curServer :: ServerAddr
+	, undeclareds :: [Undeclared]
+	, cacheOnly :: Bool
+	, curAuth :: Maybe Auth
+	, codenameStack :: [Codename]
+	, newAsync :: TVar Bool
+	, asyncError :: TVar (Maybe String)
+	, randomCodenames :: TVar [Codename]
+	, userInfos :: Map Codename (TVar FetchedRecord, UTCTime)
+	, indexedLocks :: Map LockSpec (TVar FetchedRecord)
+	, retiredLocks :: Maybe [LockSpec]
+	, curLockPath :: FilePath
+	, curLock :: Maybe (Lock,Maybe Solution)
+	, listOffset :: Int
+	}
+
+newPlayState (frame,st) title sub = PlayState st frame [] False False [] [] title sub Map.empty
+newReplayState st soln title = ReplayState st soln [] title Map.empty
+newEditState (frame,st) msoln mpath = EditState [st] [] frame mpath
+    ((\s->(st,s))<$>msoln) (Just (st, isJust msoln)) Nothing (PHS zero) (PHS zero) Map.empty
+initMetaState = do
+    flag <- atomically $ newTVar False
+    errtvar <- atomically $ newTVar Nothing
+    rnamestvar <- atomically $ newTVar []
+    (saddr, auth, path) <- confFilePath "metagame.conf" >>=
+	liftM (fromMaybe (defaultServerAddr, Nothing, "")) . readReadFile
+    let names = maybeToList $ authUser <$> auth
+    undecls <- if nullSaddr saddr then return [] else
+	confFilePath ("undeclared" ++ [pathSeparator] ++ saddrStr saddr) >>=
+	liftM (fromMaybe []) . readReadFile
+    mlock <- fullLockPath path >>= readLock
+    return $ MetaState saddr undecls False auth names flag errtvar rnamestvar Map.empty Map.empty Nothing path mlock 0
+
+readLock :: FilePath -> IO (Maybe (Lock, Maybe Solution))
+readLock path = runMaybeT $ msum
+	[ (\l->(l,Nothing)) <$> (MaybeT $ readReadFile path)
+	, do
+	    (mlock,msoln) <- lift $ readAsciiLockFile path
+	    lock <- MaybeT $ return mlock
+	    return $ (lock,msoln) ]
+-- writeLock :: FilePath -> Lock -> IO ()
+-- writeLock path lock = fullLockPath path >>= flip writeReadFile lock
+fullLockPath path = if isAbsolute path
+    then return path
+    else (++(pathSeparator:path)) <$> confFilePath "locks"
+
+writeMetaState (MetaState { curServer=saddr, undeclareds=undecls, curAuth=auth, curLockPath=path }) = do
+    confFilePath "metagame.conf" >>= flip writeReadFile (saddr, auth, path)
+    unless (nullSaddr saddr) $
+	confFilePath ("undeclared" ++ [pathSeparator] ++ saddrStr saddr) >>= flip writeReadFile undecls
+
+ms2im :: MainState -> InputMode
+ms2im mainSt = case mainSt of
+    PlayState {} -> IMPlay
+    ReplayState {} -> IMReplay
+    EditState {} -> IMEdit
+    MetaState {} -> IMMeta
+
+getTitle :: UIMonad uiM => StateT MainState uiM (Maybe String)
+getTitle = ms2im <$> get >>= \im -> case im of
+    IMEdit -> do
+	mpath <- gets esPath
+	unsaved <- editStateUnsaved
+	isTested <- isJust <$> getCurTestSoln
+	return $ Just $ "editing " ++ fromMaybe "[unnamed lock]" mpath ++ 
+	    (if isTested then " (Tested)" else "") ++
+	    (if unsaved then " [+]" else "    ")
+    IMPlay -> gets psTitle
+    IMReplay -> gets rsTitle
+    _ -> return Nothing
+
+editStateUnsaved :: UIMonad uiM => StateT MainState uiM Bool
+editStateUnsaved = (isNothing <$>) $ runMaybeT $ do
+    (sst,tested) <- MaybeT $ gets lastSavedState
+    st <- lift $ gets $ head.esGameStateStack
+    guard $ sst == st
+    nowTested <- isJust <$> lift getCurTestSoln
+    guard $ tested == nowTested
+
+getCurTestSoln :: UIMonad uiM => StateT MainState uiM (Maybe Solution)
+getCurTestSoln = runMaybeT $ do
+    (st',soln) <- MaybeT $ gets esTested
+    st <- lift $ gets $ head.esGameStateStack
+    guard $ st == st'
+    return soln
+
+instance Error () where noMsg = ()
+
+mgetOurName :: (UIMonad uiM) => MaybeT (StateT MainState uiM) Codename
+mgetOurName = MaybeT $ (authUser <$>) <$> gets curAuth
+mgetCurName :: (UIMonad uiM) => MaybeT (StateT MainState uiM) Codename
+mgetCurName = MaybeT $ listToMaybe <$> gets codenameStack 
+
+getUInfoFetched :: UIMonad uiM => Integer -> Codename -> StateT MainState uiM FetchedRecord
+getUInfoFetched staleTime name =
+    gets ((Map.lookup name) . userInfos) >>=
+	\x -> case x of
+	    Nothing -> set
+	    Just (tvar, time) -> do
+		now <- liftIO getCurrentTime
+		if floor (diffUTCTime now time) > staleTime
+		    then set
+		    else liftIO $ atomically $ readTVar tvar
+    where set = do
+	    tvar <- setUInfoTVar name
+	    liftIO $ atomically $ readTVar tvar
+
+mgetUInfo :: UIMonad uiM => Codename -> MaybeT (StateT MainState uiM) UserInfo
+mgetUInfo name = do
+    RCUserInfo (_,uinfo) <- MaybeT $ (fetchedRC <$>) $ getUInfoFetched defaultStaleTime name
+    return uinfo
+    where defaultStaleTime = 300
+
+setUInfoTVar :: UIMonad uiM => Codename -> StateT MainState uiM (TVar FetchedRecord)
+setUInfoTVar name = do
+    now <- liftIO getCurrentTime
+    tvar <- getRecordCachedFromCur True $ RecUserInfo name
+    modify $ \ms -> ms {userInfos = Map.insert name (tvar, now) $ userInfos ms}
+    return tvar
+
+invalidateUInfo :: UIMonad uiM => Codename -> StateT MainState uiM ()
+invalidateUInfo name = 
+    modify $ \ms -> ms {userInfos = Map.delete name $ userInfos ms}
+
+mgetLock :: UIMonad uiM => LockSpec -> MaybeT (StateT MainState uiM) Lock
+mgetLock ls = do
+    tvar <- msum [ MaybeT $ (Map.lookup ls) <$> gets indexedLocks 
+	, lift $ do
+	    tvar <- getRecordCachedFromCur True $ RecLock ls 
+	    modify $ \ms -> ms { indexedLocks = Map.insert ls tvar $ indexedLocks ms }
+	    return tvar ]
+    RCLock lock <- MaybeT $ (fetchedRC<$>) $ liftIO $ atomically $ readTVar tvar
+    return $ reframe lock
+
+refreshUInfoUI :: (UIMonad uiM) => StateT MainState uiM ()
+refreshUInfoUI = void.runMaybeT $ do
+    modify $ \ms -> ms { listOffset = 0 }
+    mourNameSelected >>= flip when getRandomNames
+    lift $ modify $ \ms -> ms {retiredLocks = Nothing}
+    lift.lift $ drawMessage ""
+    where
+	getRandomNames = do
+	    rnamestvar <- gets randomCodenames
+	    flag <- gets newAsync
+	    saddr <- gets curServer
+	    void $ liftIO $ forkIO $ do
+		resp <- makeRequest saddr $
+		    ClientRequest protocolVersion Nothing $ GetRandomNames 19
+		case resp of
+		    ServedRandomNames names -> atomically $ do 
+			    writeTVar rnamestvar names
+			    writeTVar flag True
+		    _ -> return ()
+
+mourNameSelected :: (UIMonad uiM) => MaybeT (StateT MainState uiM) Bool
+mourNameSelected = liftM2 (==) mgetCurName mgetOurName
+
+purgeInvalidUndecls :: (UIMonad uiM) => StateT MainState uiM ()
+purgeInvalidUndecls = do
+    undecls' <- gets undeclareds >>= filterM ((not<$>).invalid)
+    modify $ \ms -> ms { undeclareds = undecls' }
+    where
+	invalid (Undeclared _ ls (ActiveLock name idx)) =
+	    (fromMaybe False <$>) $ runMaybeT $ do
+		uinfo <- mgetUInfo name
+		ourName <- mgetOurName
+		msum [ do
+			linfo <- MaybeT $ return $ userLocks uinfo ! idx
+			return $ public linfo 
+			    || ourName `elem` accessedBy linfo 
+			    || lockSpec linfo /= ls
+		    , return True
+		    ]
+
+curServerAction :: UIMonad uiM => Protocol.Action -> StateT MainState uiM ServerResponse
+curServerAction act = do
+    saddr <- gets curServer
+    auth <- gets curAuth
+    cOnly <- gets cacheOnly
+    if cOnly then return $ ServerError "Can't contact server in cache-only mode"
+	else lift $ withImpatience $ liftIO $ makeRequest saddr $ ClientRequest protocolVersion auth act
+
+curServerActionAsync :: UIMonad uiM => Protocol.Action -> StateT MainState uiM ()
+curServerActionAsync act = do
+    saddr <- gets curServer
+    auth <- gets curAuth
+    flag <- gets newAsync
+    errtvar <- gets asyncError
+    cOnly <- gets cacheOnly
+    void $ liftIO $ forkIO $ do
+	resp <- if cOnly then return $ ServerError "Can't contact server in cache-only mode"
+		else makeRequest saddr $ ClientRequest protocolVersion auth act
+	case resp of
+	    ServerError err -> atomically $ writeTVar errtvar $ Just err
+	    _ -> return ()
+	atomically $ writeTVar flag True
+
+checkAsyncErrors :: UIMonad uiM => StateT MainState uiM ()
+checkAsyncErrors = void.runMaybeT $ do
+    errtvar <- lift $ gets asyncError
+    err <- MaybeT $ liftIO $ atomically $ readTVar errtvar
+    lift.lift $ drawError err
+    liftIO $ atomically $ writeTVar errtvar Nothing
+
+getRecordCachedFromCur :: UIMonad uiM => Bool -> Record -> StateT MainState uiM (TVar FetchedRecord)
+getRecordCachedFromCur flagIt rec = do
+    saddr <- gets curServer
+    auth <- gets curAuth
+    cOnly <- gets cacheOnly
+    flag <- gets newAsync
+    liftIO $ getRecordCached saddr auth
+	(if flagIt then Just flag else Nothing) cOnly rec
+
+getFreshRecBlocking :: UIMonad uiM => Record -> StateT MainState uiM (Maybe RecordContents)
+getFreshRecBlocking rec = do
+    tvar <- getRecordCachedFromCur False rec
+    cOnly <- gets cacheOnly
+    fetched <- lift $ withImpatience $ liftIO $ atomically $ do
+	fetched@(FetchedRecord fresh _ _) <- readTVar tvar
+	check $ fresh || cOnly
+	return fetched
+    case fetchError fetched of
+	Nothing -> return $ fetchedRC fetched
+	Just err -> lift (drawError err) >> return Nothing
+
+-- |draw indication that we're waiting for the server while we do so
+withImpatience :: UIMonad uiM => uiM a -> uiM a
+withImpatience m = do
+    drawImpatience <- getDrawImpatience
+    finishedTV <- liftIO $ atomically $ newTVar False
+    let waitImpatiently ticks = do
+	    wakeTV <- atomically $ newTVar False
+	    forkIO $ threadDelay (10^6) >> atomically (writeTVar wakeTV True)
+	    finished <- atomically $ do
+		wake <- readTVar wakeTV
+		finished <- readTVar finishedTV
+		check $ wake || finished
+		return finished
+	    when (not finished) $ drawImpatience (ticks+1) >> waitImpatiently (ticks+1)
+    liftIO $ forkIO $ waitImpatiently 0
+    m <* (liftIO $ atomically $ writeTVar finishedTV True)
+
+getRelScore :: (UIMonad uiM) => Codename -> StateT MainState uiM (Maybe Int)
+getRelScore name = runMaybeT $ do
+    ourName <- mgetOurName
+    guard $ ourName /= name
+    uinfo <- mgetUInfo name
+    ourUInfo <- mgetUInfo ourName
+    return $ countUnaccessedBy ourUInfo name - countUnaccessedBy uinfo ourName
+    where
+	countUnaccessedBy ui name = length $ filter (not.snd) $ getAccessInfo ui name 
+
+getNotesReadOn :: UIMonad uiM => LockInfo -> StateT MainState uiM [NoteInfo]
+getNotesReadOn lockinfo = (fromMaybe [] <$>) $ runMaybeT $ do
+    ourName <- mgetOurName
+    ourUInfo <- mgetUInfo ourName
+    return $ filter (\n -> isNothing (noteBehind n)
+	    || n `elem` notesRead ourUInfo) $ lockSolutions lockinfo
+
+testAuth :: UIMonad uiM => StateT MainState uiM ()
+testAuth = (isJust <$> gets curAuth >>=) $ flip when $ do
+    resp <- curServerAction $ Authenticate
+    case resp of
+	ServerMessage msg -> (lift $ drawMessage $ "Server: " ++ msg)
+	ServerError err -> do
+	    lift $ drawMessage err
+	    modify $ \ms -> ms {curAuth = Nothing}
+	_ -> return ()
diff --git a/Maxlocksize.hs b/Maxlocksize.hs
new file mode 100644
--- /dev/null
+++ b/Maxlocksize.hs
@@ -0,0 +1,15 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module Maxlocksize where
+
+-- | Locks bigger than 11 won't fit in an 80x25 terminal
+maxlocksize :: Int
+maxlocksize = 11
diff --git a/Metagame.hs b/Metagame.hs
new file mode 100644
--- /dev/null
+++ b/Metagame.hs
@@ -0,0 +1,148 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module Metagame where
+
+import Control.Applicative
+import Data.Array
+import Data.Binary
+import Control.Monad
+import Data.List (delete)
+import Data.Char
+
+import GameStateTypes
+import Lock
+
+notesNeeded = 3
+maxLocks = 3
+
+type Codename = String
+
+validCodeName name = length name == 3 && all validChar name
+    where validChar c = isAscii c && isPrint c && not (isLower c) && c /= ' '
+
+data UserInfo = UserInfo {codename::Codename, userLocks::Array LockIndex (Maybe LockInfo), notesRead::[NoteInfo]}
+    deriving (Eq, Ord, Show, Read)
+
+initUserInfo name = UserInfo name (array (0,maxLocks-1) $ zip [0..maxLocks-1] (repeat Nothing)) []
+
+data LockInfo = LockInfo {lockSpec::LockSpec, public::Bool, notesSecured::[NoteInfo], lockSolutions::[NoteInfo], accessedBy::[Codename]}
+    deriving (Eq, Ord, Show, Read)
+
+initLockInfo ls = LockInfo ls False [] [] []
+
+getAccessInfo :: UserInfo -> Codename -> [(Bool,Bool)]
+-- | getAccessInfo ui name = [(ith lock is public, ith lock is accessed by name) | i]
+getAccessInfo ui name =
+    let mlinfos = elems $ userLocks ui
+	accessedSlot = maybe accessedAllExisting accessedLock
+	accessedAllExisting = all (maybe True accessedLock) mlinfos
+	accessedLock linfo = public linfo || name `elem` accessedBy linfo 
+    in map (\mlinfo -> (maybe False public mlinfo, accessedSlot mlinfo)) mlinfos
+
+data UserInfoDelta
+    = AddRead NoteInfo
+    | DelRead NoteInfo
+    | PutLock LockSpec LockIndex
+    | LockDelta LockIndex LockDelta
+    deriving (Eq, Ord, Show, Read)
+data LockDelta
+    = SetPubNote NoteInfo
+    | AddSecured NoteInfo
+    | DelSecured NoteInfo
+    | AddSolution NoteInfo
+    | AddAccessed Codename
+    | SetPublic
+    deriving (Eq, Ord, Show, Read)
+
+data NoteInfo = NoteInfo {noteAuthor::Codename, noteBehind::Maybe ActiveLock, noteOn::ActiveLock} 
+    deriving (Eq, Ord, Show, Read)
+
+data ActiveLock = ActiveLock {lockOwner::Codename, lockIndex :: LockIndex}
+    deriving (Eq, Ord, Show, Read)
+
+data Undeclared = Undeclared Solution LockSpec ActiveLock
+    deriving (Eq, Ord, Show, Read)
+
+-- | permanent serial number of a lock
+type LockSpec = Int
+
+-- | which of a user's three locks (0,1, or 2)
+type LockIndex = Int
+
+-- | solved state
+type Hint = GameState
+
+lockIndexChar :: LockIndex -> Char
+lockIndexChar i = toEnum $ i + fromEnum 'A' 
+charLockIndex c = fromEnum (toUpper c) - fromEnum 'A'
+
+applyDeltas :: UserInfo -> [UserInfoDelta] -> UserInfo
+applyDeltas = foldr applyDelta
+
+applyDelta :: UserInfoDelta -> UserInfo -> UserInfo
+applyDelta (AddRead n) info = info { notesRead = n:(notesRead info) }
+applyDelta (DelRead n) info = info { notesRead = delete n (notesRead info) }
+applyDelta (PutLock ls li) info = info { userLocks = userLocks info // [(li, Just $ initLockInfo ls)] }
+applyDelta (LockDelta li ld) info =
+    info { userLocks = userLocks info // [(li, liftM (applyLockDelta ld) (userLocks info ! li))] }
+applyLockDelta (SetPubNote n) lockinfo = lockinfo { lockSolutions = map
+    (\n' -> if n' == n then n {noteBehind=Nothing} else n') (lockSolutions lockinfo) }
+applyLockDelta (AddSecured n) lockinfo = lockinfo { notesSecured = n:(notesSecured lockinfo) }
+applyLockDelta (DelSecured n) lockinfo = lockinfo { notesSecured = delete n $ notesSecured lockinfo }
+applyLockDelta (AddSolution n) lockinfo = lockinfo { lockSolutions = n:(lockSolutions lockinfo) }
+applyLockDelta (AddAccessed name) lockinfo = lockinfo { accessedBy = name:(delete name $ accessedBy lockinfo) }
+applyLockDelta SetPublic lockinfo = lockinfo { public = True, lockSolutions = [], accessedBy = []}
+
+instance Binary UserInfo where
+    put (UserInfo name locks notes) = put name >> put locks >> put notes
+    get = liftM3 UserInfo get get get
+
+instance Binary UserInfoDelta where
+    put (AddRead note) = put (0::Word8) >> put note
+    put (DelRead note) = put (1::Word8) >> put note
+    put (PutLock ls li) = put (2::Word8) >> put ls >> put li
+    put (LockDelta li ld) = put (3::Word8) >> put li >> put ld
+    get = do
+	tag <- get :: Get Word8
+	case tag of
+	    0 -> AddRead <$> get
+	    1 -> DelRead <$> get
+	    2 -> PutLock <$> get <*> get
+	    3 -> LockDelta <$> get <*> get
+
+instance Binary LockDelta where
+    put (SetPubNote note) = put (0::Word8) >> put note
+    put (AddSecured note) = put (1::Word8) >> put note
+    put (DelSecured note) = put (2::Word8) >> put note
+    put (AddSolution note) = put (3::Word8) >> put note
+    put (AddAccessed name) = put (4::Word8) >> put name
+    put SetPublic = put (5::Word8)
+    get = do
+	tag <- get :: Get Word8
+	case tag of
+	    0 -> SetPubNote <$> get
+	    1 -> AddSecured <$> get
+	    2 -> DelSecured <$> get
+	    3 -> AddSolution <$> get
+	    4 -> AddAccessed <$> get
+	    5 -> return SetPublic
+
+instance Binary LockInfo where
+    put (LockInfo spec pk notes solved accessed) = put spec >> put pk >> put notes >> put solved >> put accessed
+    get = liftM5 LockInfo get get get get get 
+
+instance Binary NoteInfo where
+    put (NoteInfo author behind on) = put author >> put behind >> put on
+    get = liftM3 NoteInfo get get get
+
+instance Binary ActiveLock where
+    put (ActiveLock owner idx) = put owner >> put idx
+    get = liftM2 ActiveLock get get
diff --git a/Mundanities.hs b/Mundanities.hs
new file mode 100644
--- /dev/null
+++ b/Mundanities.hs
@@ -0,0 +1,72 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module Mundanities where
+import Control.Applicative
+import Control.Arrow
+import Control.Monad
+import qualified Control.Exception as E
+import System.Directory
+import System.FilePath
+import Data.Maybe
+import Data.List
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Char8 as BSC
+import Paths_intricacy
+
+-- | This is "catchIOError" in >=base-4.4, but we reimplement it to support
+-- older bases.
+catchIO :: IO a -> (IOError -> IO a) -> IO a
+catchIO = E.catch
+
+readReadFile :: (Read a) => FilePath -> IO (Maybe a)
+readReadFile file =
+    (tryRead . BSC.unpack <$> BS.readFile file)
+	`catchIO` (const $ return Nothing)
+tryRead :: (Read a) => String -> Maybe a
+tryRead = (fst <$>) . listToMaybe . reads
+
+readStrings :: FilePath -> IO [String]
+readStrings = (lines . BSC.unpack <$>) . BS.readFile
+
+writeReadFile :: (Show a) => FilePath -> a -> IO ()
+writeReadFile file x = do
+    mkdirhierto file
+    BS.writeFile file $ BSC.pack $ show x
+
+writeStrings :: FilePath -> [String] -> IO ()
+writeStrings file x = do
+    mkdirhierto file
+    BS.writeFile file $ BSC.pack $ unlines x
+
+confFilePath :: FilePath -> IO FilePath
+confFilePath str =
+    (++(pathSeparator:str)) <$> getAppUserDataDirectory "intricacy"
+
+getDataPath :: FilePath -> IO FilePath
+getDataPath = getDataFileName
+
+makeConfDir :: IO ()
+makeConfDir = confFilePath "" >>= createDirectoryIfMissing False
+
+mkdirhierto :: FilePath -> IO ()
+mkdirhierto = mkdirhier . takeDirectory
+
+mkdirhier :: FilePath -> IO ()
+mkdirhier path = do
+    exists <- doesDirectoryExist path
+    unless exists $ mkdirhierto path >> createDirectory path
+
+getDirContentsRec :: FilePath -> IO [FilePath]
+getDirContentsRec path = flip catchIO (const $ return []) $ do
+    contents <- map ((path++[pathSeparator])++) <$> filter ((/='.').head) <$> getDirectoryContents path
+    annotated <- (\p -> (,) p <$> doesDirectoryExist p) `mapM` contents
+    let (dirs,files) = join (***) (map fst) $ partition snd annotated
+    (files++) . concat <$> getDirContentsRec `mapM` dirs
diff --git a/NEWS b/NEWS
new file mode 100644
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,53 @@
+0.3:
+    Minor fixes; first version uploaded to hackage
+0.2.8:
+    Implemented click-and-drag movement in play and edit modes.
+0.2.7:
+    Improvements to curses UI - handle resizing; show keys; allow rebinding.
+    Fixed some bugs with viewing notes.
+0.2.6.3:
+    Update default server address
+0.2.6.2:
+    Fix silly crash bug introduced by 0.2.6.1
+0.2.6.1:
+    Add mouse control of tools
+0.2.6:
+    Minor UI tweaks.
+0.2.5:
+    Shortened tutorial drastically. Other levels now in 'tutorial-extra/'.
+    Improved video setup handling.
+    Fixed bug with text disappearing on resize.
+0.2.4:
+    Improve handling inaccessible servers.
+    Added background.
+    Fixed bug with prompts being quickly dismissed when using the mouse.
+0.2.3:
+    Minor graphical improvements.
+0.2.2:
+    Visible changes:
+	scoring tweak: empty slots are accessed iff all non-empty slots are;
+	'q'uit confirmation now reminds you what you're quitting from;
+	we now consider testedness when deciding if a lock is modified;
+	added detailed game info to the README;
+	added hover text for UI buttons.
+    Bugs fixed:
+	editor, lockfile reader, and validity checker were not in accordance
+	    on what is allowed as the root of a spring;
+	SDL UI was redrawing the screen on every mouse movement;
+	curses UI wasn't refreshing properly on async flag.
+0.2.1:
+    New features:
+	The "reset tools" command has become a "test lock" command. Successful
+	    tests are saved along with the lock, meaning you can then place the
+	    lock without having to test it again.
+
+	You can now use marks in 'play' mode, and there's a default mark for the
+	    initial state, effectively giving a "reset" command.
+
+    Bugs fixed: 
+	stale notes weren't getting refreshed
+	retired display got stuck on changing codename
+	you couldn't move into the bolthole with keyboard commands
+    
+0.2:
+    First public release
diff --git a/Physics.hs b/Physics.hs
new file mode 100644
--- /dev/null
+++ b/Physics.hs
@@ -0,0 +1,267 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module Physics where
+
+import qualified Data.Set as Set
+import Data.Set (Set)
+import qualified Data.Vector as Vector
+import Control.Monad.State
+import Control.Monad.Writer
+import Data.Foldable (foldrM)
+import Data.List
+import Data.Vector (Vector, (!), (//))
+
+import Hex
+import Util
+import GameState
+import GameStateTypes
+
+-- | a list of forces to try in order:
+newtype ForceChoice = ForceChoice {getForceChoice :: [Force]}
+
+type ForceChoices = Vector.Vector ForceChoice
+
+forceIdx :: Force -> PieceIdx
+forceIdx force = case force of (Push idx _) -> idx
+			       (Torque idx _) -> idx
+
+isPush,isTorque,forceIsNull :: Force -> Bool
+isPush (Push _ _) = True
+isPush _ = False
+isTorque = not . isPush
+forceIsNull (Push _ dir) = dir == zero
+forceIsNull (Torque _ dir) = dir == 0
+
+getForcedpp :: GameState -> Force -> PlacedPiece
+getForcedpp s f = getpp s (forceIdx f)
+
+-- |PlayerMove: if not NullPM, the direction should be non-zero
+data PlayerMove = NullPM | HookPush HexDir | HookTorque TorqueDir | WrenchPush HexDir
+    deriving (Eq, Ord, Show, Read)
+
+toolForces :: GameState -> PlayerMove -> ForceChoices
+toolForces st pm = Vector.fromList $
+    [ ForceChoice (wmom++wmove)
+    | (widx, PlacedPiece _ (Wrench mom)) <- epps
+    , let wmom = if mom == zero then [] else [Push widx mom]
+    , let wmove = case pm of {WrenchPush v -> [Push widx v]; _ -> []}
+    , not $ null (wmom++wmove)
+    ] ++ case pm of
+	HookTorque ht -> [ ForceChoice [Torque hidx ht] | hidx <- hidxs ]
+	HookPush hp -> [ ForceChoice [Push hidx hp] | hidx <- hidxs ]
+	_ -> []
+    where
+	epps = enumVec $ placedPieces st
+	hidxs = [ hidx | (hidx, PlacedPiece _ (Hook _ _)) <- epps ]
+
+-- |Dominance: a propagand of a source force F can not block any progagand of
+-- a source force which dominates F.
+-- F dominates F' iff F and F' are spring forces, and the end of F is an
+-- ancestor of the root of F'.
+-- Note that by acyclicity of the connection digraph, domination is
+-- antisymmetric.
+type Dominance = Int -> Int -> Bool
+
+envForces :: GameState -> (ForceChoices, Dominance)
+envForces st@(GameState _ conns) =
+    let rootedForces :: Vector (Maybe PieceIdx, Force)
+	rootedForces = Vector.fromList [ (Just rootIdx, Push endIdx dir)
+	    | c@(Connection (rootIdx,_) (endIdx,_) (Spring outDir natLen)) <- conns
+	    , let curLen = connectionLength st c
+	    , natLen /= curLen
+	    , let dir = if natLen > curLen then outDir else neg outDir ]
+
+    in ( Vector.map (ForceChoice . replicate 1 . snd) rootedForces, 
+	\f1 f2 -> Just True == do
+		rootIdx <- fst $ rootedForces!f2
+		return $ connGraphPathExists st (forceIdx.snd $ rootedForces!f1) rootIdx )
+	
+
+setTools :: PlayerMove -> GameState -> GameState
+setTools pm st =
+    let hf = case pm of
+	    HookTorque dir -> TorqueHF dir
+	    HookPush v -> PushHF v
+	    _ -> NullHF
+    in adjustPieces (\p -> case p of
+		Hook arm _ -> Hook arm hf
+		_ -> p) st
+
+physicsTick :: PlayerMove -> GameState -> Writer [Alert] GameState
+physicsTick pm st =
+    let tfs = toolForces st pm
+	(efs, dominates) = envForces st
+    in do
+	st' <- resolveForces tfs Vector.empty (\_ _->False) $ setTools pm st
+	resolveForces Vector.empty efs dominates $ setTools NullPM st'
+
+type Source = Int
+data SourcedForce = SForce Source Force Bool Bool
+    deriving (Eq, Ord, Show)
+resolveForces :: ForceChoices -> ForceChoices -> Dominance -> GameState -> Writer [Alert] GameState
+resolveForces plForces eForces eDominates st =
+    let pln = Vector.length plForces
+	dominates i j = case map (< pln) [i,j] of
+		[True,False] -> True
+		[False,False] -> eDominates (i-pln) (j-pln)
+		_ -> False
+	initGrps = fmap (propagate st True) plForces Vector.++
+		fmap (propagate st False) eForces 
+	blockInconsistent :: Int -> Int -> StateT (Vector (Writer Any [Force])) (Writer [Alert]) ()
+	blockInconsistent i j = do
+	    grps <- mapM gets [(!i),(!j)]
+	    blocks <- lift $ checkInconsistent i j $ map (fst.runWriter) grps
+	    modify $ Vector.imap (\k -> if k `elem` blocks then (tell (Any True) >>) else id)
+	checkInconsistent :: Int -> Int -> [[Force]] -> Writer [Alert] [Int]
+	checkInconsistent i j fss = 
+	    let st' = foldr applyForce st $ nub $ concat fss
+		inconsistencies = [ [f,f']
+		    | [f,f'] <- sequence fss
+		    , if forceIdx f == forceIdx f'
+			then f /= f'
+			else not.null $ collisions st' (forceIdx f) (forceIdx f') ]
+	    in do
+		tell $ map AlertBlockingForce $ concat inconsistencies
+		return $ if null inconsistencies then []
+		    else if i==j then [i]
+		    else if dominates i j then [j]
+		    else if dominates j i then [i]
+		    else [i,j]
+
+	stopWrench idx = setPiece idx (Wrench zero)
+	stopBlockedWrenches fs st' = foldr stopWrench st' [
+	    idx | f <- fs, let idx = forceIdx f, isWrench.placedPiece $ getpp st' idx ]
+    in do 
+	let unresisted = [ s | (s, (_, Any False)) <- enumVec $ fmap runWriter initGrps ]
+
+	-- check for inconsistencies within, and between pairs of, forcegroups
+	grps <- sequence [ blockInconsistent i j
+			| [i,j] <- sequence [unresisted,unresisted]
+			, i <= j ]
+		    `execStateT` initGrps
+
+	let [blocked, unblocked] = map (nub.concat.(map (fst.runWriter)).Vector.toList) $
+		(\(x,y) -> [x,y]) $ Vector.partition (getAny.snd.runWriter) grps
+	tell $ map AlertBlockedForce blocked
+	return $ stopBlockedWrenches blocked $ foldr applyForce st unblocked
+    
+applyForce :: Force -> GameState -> GameState
+applyForce f s =
+    let idx = forceIdx f
+	pp' = applyForceTo (getpp s idx) f
+	pp'' = case (placedPiece pp',f) of
+		   ( Wrench _ , Push _ dir ) -> pp' {placedPiece = Wrench dir}
+		   _ -> pp'
+	in 
+	s { placedPieces = (placedPieces s) // [(idx, pp'')] }
+
+collisionsWithForce :: GameState -> Force -> PieceIdx -> [HexPos]
+collisionsWithForce st (Push idx dir) idx' =
+    intersect (map (dir<+>) $ footprintAtIgnoring st idx idx') (footprintAtIgnoring st idx' idx)
+collisionsWithForce st force idx' =
+    collisions (applyForce force st) (forceIdx force) idx'
+
+applyForceTo :: PlacedPiece -> Force -> PlacedPiece
+applyForceTo (PlacedPiece pos piece) (Push _ dir) =
+    PlacedPiece (dir <+> pos) piece
+applyForceTo (PlacedPiece pos (Pivot arms)) (Torque _ dir) =
+    PlacedPiece pos (Pivot $ map (rotate dir) arms)
+applyForceTo (PlacedPiece pos (Hook arm hf)) (Torque _ dir) =
+    PlacedPiece pos (Hook (rotate dir arm) hf)
+applyForceTo pp _ = pp
+
+-- A force on a piece which resists it is immediately blocked
+pieceResists :: GameState -> Force -> Bool 
+pieceResists st force =
+    let idx = forceIdx force
+	PlacedPiece _ piece = getpp st idx
+        springs = springsEndAtIdx st idx
+	fixed = case piece of
+		     (Pivot _) -> isPush force
+		     (Block _) -> null springs
+		     (Wrench mom) -> case force of
+			    Push _ v -> v /= mom
+			    _ -> True
+		     (Hook _ hf) -> case force of 
+			    Push _ v -> hf /= PushHF v
+			    Torque _ dir -> hf /= TorqueHF dir
+		     _ -> False
+    in fixed
+
+-- |transmittedForce: convert pushes into torques as appropriate
+transmittedForce :: GameState -> Source -> HexPos -> HexDir -> Force
+transmittedForce st idx cpos dir =
+    let pp@(PlacedPiece _ piece) = getpp st idx
+	rpos = cpos <-> placedPos pp
+	armPush = case
+		(dir `hexDot` ((rotate 1 rpos) <-> rpos)) `compare`
+		(dir `hexDot` ((rotate (-1) rpos) <-> rpos)) of
+	    GT -> Torque idx 1
+	    LT -> Torque idx $ -1
+	    EQ -> Push idx dir
+    in case piece of
+	    Pivot _ -> armPush
+	    Hook _ (TorqueHF _) -> armPush
+	    _ -> (Push idx dir)
+
+-- |propagateForce: return forces a force causes via bumps and fully
+-- compressed/extended springs
+propagateForce :: GameState -> Bool -> Force -> [ForceChoice]
+propagateForce st@(GameState _ conns) isPlSource force =
+    bumps ++ springTransmissions
+    where
+	idx = forceIdx force
+	bumps = [ ForceChoice $ map (transmittedForce st idx' cpos) dirs |
+	    idx' <- ppidxs st
+	    , idx' /= idx
+	    , cpos <- collisionsWithForce st force idx'
+	    , let dirs = case force of
+		    Push _ dir -> [dir]
+		    Torque _ dir -> [push,claw]
+			where push = arm <-> rotate (-dir) arm
+			      claw = rotate dir arm <-> arm
+			      arm = cpos <-> placedPos (getpp st idx) ]
+	springTransmissions =
+	    case force of
+		 Push _ dir -> [ ForceChoice [Push idx' dir] |
+			c@(Connection (ridx,_) (eidx,_) (Spring sdir _)) <- conns
+			, let root = idx == ridx
+			, let end = idx == eidx
+			, root || end
+			, let idx' = if root then eidx else ridx
+			, let pull = (root && dir == neg sdir) || (end && dir == sdir)
+			, let push = (root && dir == sdir) || (end && dir == neg sdir)
+			, (push && if isPlSource then springFullyCompressed st c else not $ springExtended st c) ||
+			    (pull && if isPlSource then springFullyExtended st c else not $ springCompressed st c) ||
+			    (not push && not pull) ]
+		 _ -> []
+
+-- |propagate: find forcegroup generated by a forcechoice, and note if the
+-- group is blocked due to resistance. If there are multiple forces in a
+-- forcechoice and the first results in a block due to resistance, try the
+-- next instead.
+propagate :: GameState -> Bool -> ForceChoice -> Writer Any [Force]
+propagate st isPlSource fch = Set.toList `liftM` propagate' isPlSource Set.empty fch where
+    propagate' isPlForce ps (ForceChoice (f:backups)) =
+	if f `Set.member` ps then return ps
+	else
+	    let (ps', failed) = if pieceResists st f && (not isPlForce)
+		    then (ps, Any True)
+		    else runWriter $ foldrM
+			    (flip $ propagate' False)
+			    (f `Set.insert` ps)
+			    $ propagateForce st isPlSource f
+	    in if getAny failed
+		then if null backups
+		    then tell (Any True) >> return ps'
+		    else propagate' isPlForce ps $ ForceChoice backups
+		else return ps'
+    propagate' _ _ (ForceChoice []) = error "null ForceChoice"
diff --git a/Protocol.hs b/Protocol.hs
new file mode 100644
--- /dev/null
+++ b/Protocol.hs
@@ -0,0 +1,144 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module Protocol where
+
+import Data.Binary
+import Control.Monad
+
+import BinaryInstances
+import Metagame
+import Lock
+
+type ProtocolVersion = Int
+protocolVersion = 1 :: ProtocolVersion
+
+data ClientRequest = ClientRequest ProtocolVersion (Maybe Auth) Action
+    deriving (Eq, Ord, Show, Read)
+
+type VersionedUInfo = (Int, UserInfo)
+
+data Action
+    = Authenticate
+    | Register
+    | ResetPassword Password
+    | GetServerInfo
+    | GetLock LockSpec
+    | GetRetired Codename
+    | GetUserInfo Codename (Maybe Int)
+    | GetHint NoteInfo
+    | GetSolution NoteInfo
+    | DeclareSolution Solution LockSpec ActiveLock LockIndex
+    | SetLock Lock LockIndex Solution
+    | GetRandomNames Int
+    deriving (Eq, Ord, Show, Read)
+
+data Auth = Auth {authUser :: Codename, authPasswd :: Password}
+    deriving (Eq, Ord, Show, Read)
+type Password = String
+
+needsAuth :: Action -> Bool
+needsAuth GetServerInfo = False
+needsAuth (GetLock _) = False
+needsAuth (GetUserInfo _ _) = False
+needsAuth (GetRetired _) = False
+needsAuth (GetRandomNames _) = False
+needsAuth _ = True
+
+data ServerResponse
+    = ServerAck
+    | ServerMessage String
+    | ServerError String
+    | ServedServerInfo ServerInfo
+    | ServedLock Lock
+    | ServedRetired [LockSpec]
+    | ServedUserInfo VersionedUInfo
+    | ServedUserInfoDeltas [UserInfoDelta]
+    | ServedSolution Solution
+    | ServedHint Hint
+    | ServedRandomNames [Codename]
+    | ServerCodenameFree
+    | ServerFresh
+    deriving (Eq, Ord, Show, Read)
+
+data ServerInfo = ServerInfo {serverLockSize :: Int, serverInfoString::String}
+    deriving (Eq, Ord, Show, Read)
+defaultServerInfo = ServerInfo 8 ""
+
+instance Binary ClientRequest where
+    put (ClientRequest pv mauth act) = putPackedInt pv >> put mauth >> put act
+    get = liftM3 ClientRequest getPackedInt get get
+
+instance Binary Action where
+    put Authenticate = put (0::Word8)
+    put Register = put (1::Word8)
+    put GetServerInfo = put (2::Word8)
+    put (GetLock lspec) = put (3::Word8) >> put lspec
+    put (GetUserInfo name version) = put (4::Word8) >> put name >> put version
+    put (GetHint lspec) = put (5::Word8) >> put lspec
+    put (GetSolution lspec) = put (6::Word8) >> put lspec
+    put (DeclareSolution soln lspec alock idx) = put (7::Word8) >> put soln >> put lspec >> put alock >> put idx
+    put (SetLock lock li soln) = put (8::Word8) >> put lock >> put li >> put soln
+    put (GetRandomNames n) = put (9::Word8) >> put n
+    put (ResetPassword pw) = put (10::Word8) >> put pw
+    put (GetRetired name) = put (11::Word8) >> put name
+    get = do
+	tag <- get :: Get Word8
+	case tag of
+	    0 -> return Authenticate
+	    1 -> return Register
+	    2 -> return GetServerInfo
+	    3 -> liftM GetLock get
+	    4 -> liftM2 GetUserInfo get get
+	    5 -> liftM GetHint get
+	    6 -> liftM GetSolution get
+	    7 -> liftM4 DeclareSolution get get get get
+	    8 -> liftM3 SetLock get get get
+	    9 -> liftM GetRandomNames get
+	    10 -> liftM ResetPassword get
+	    11 -> liftM GetRetired get
+
+instance Binary Auth where
+    put (Auth name pw) = put name >> put pw
+    get = liftM2 Auth get get
+
+instance Binary ServerResponse where
+    put ServerAck = put (0::Word8)
+    put (ServerMessage mesg) = put (1::Word8) >> put mesg
+    put (ServerError err) = put (2::Word8) >> put err
+    put (ServedServerInfo sinfo) = put (3::Word8) >> put sinfo
+    put (ServedLock lock) = put (4::Word8) >> put lock
+    put (ServedUserInfo info) = put (5::Word8) >> put info
+    put (ServedUserInfoDeltas info) = put (6::Word8) >> put info
+    put (ServedSolution soln) = put (7::Word8) >> put soln
+    put (ServedHint hint) = put (8::Word8) >> put hint
+    put (ServedRandomNames names) = put (9::Word8) >> put names
+    put (ServerCodenameFree) = put (10::Word8)
+    put (ServerFresh) = put (11::Word8)
+    put (ServedRetired lss) = put (12::Word8) >> put lss
+    get = do
+	tag <- get :: Get Word8
+	case tag of
+	    0 -> return ServerAck
+	    1 -> liftM ServerMessage get
+	    2 -> liftM ServerError get
+	    3 -> liftM ServedServerInfo get
+	    4 -> liftM ServedLock get
+	    5 -> liftM ServedUserInfo get
+	    6 -> liftM ServedUserInfoDeltas get
+	    7 -> liftM ServedSolution get
+	    8 -> liftM ServedHint get
+	    9 -> liftM ServedRandomNames get
+	    10 -> return ServerCodenameFree
+	    11 -> return ServerFresh
+	    12 -> liftM ServedRetired get
+instance Binary ServerInfo where
+    put (ServerInfo sz str) = put sz >> put str
+    get = liftM2 ServerInfo get get
diff --git a/README b/README
new file mode 100644
--- /dev/null
+++ b/README
@@ -0,0 +1,216 @@
+Intricacy
+=========
+A game of competitive puzzle-design.
+
+http://mbays.freeshell.org/intricacy/
+
+
+Compiling
+=========
+Dependencies:
+    EITHER
+	sdl
+	sdl-ttf
+	sdl-gfx
+    OR
+	curses
+
+To compile the game on *nix with SDL graphics:
+    Install ghc, cabal, and development packages for the SDL dependencies
+    above, then run
+	cabal update && cabal install
+    which should result in various haskell packages being compiled and
+    installed, probably in ~/.cabal/ . The game can then be run as
+	~/.cabal/bin/intricacy 
+    Running cabal install as root should install it somewhere global.
+
+To compile a curses-only (ascii graphics) build:
+	cabal install -f -SDL
+
+To compile the server:
+	cabal install -f Server -f -Game
+
+    The server will be installed in
+	~/.cabal/bin/intricacy-server
+    The server runs on port 27001 by default. It writes the game database to
+    a directory 'intricacydb' under the directory from which it is run.
+
+To compile for windows:
+    This should work as above once you have the dependencies installed
+    properly. Good luck with that. The mingw32-compiled libraries in winlibs
+    may or may not help.
+
+To compile for OSX, or android or whatever else wacky system:
+    No idea, sorry. But please tell me if you manage!
+
+
+Playing
+=======
+Press 'T' on the starting screen to play the tutorials, which should lead you
+to discover the main game mechanics. When you've finished or had enough of the
+tutorials, select a codename with 'C' and register it with 'R'. The rest you
+can hopefully figure out, but see below if you'd rather read about it.
+
+
+Configuration
+=============
+You can change keybindings in SDL mode by right-clicking on the corresponding
+button and pressing a key.
+
+The buttons in the top right when solving/editing locks in SDL mode are as
+follows:
+    * The three hexes toggles between two modes for colouring pieces; in one
+	mode (three yellow hexes), the colours are determined by the nature of
+	the pieces; in the other mode, adjacent pieces always get different
+	colours.
+    * The lone hex with optional arrow toggles "blockage annotations". These
+	are there to help you figure out what's going on when you can't
+	understand why a piece refuses to move. An orange arrow indicates a
+	blocked force, and a purple arrow a force which blocked another force.
+
+Config files are saved in ~/.intricacy/ on unixoids, or something like
+"...\Application Data\intricacy\" on windows. In particular, the locks you
+create in the lock editor are saved in there (and you can organise them into
+directories by including (back)slashs in the lock name). You can edit them in
+a text editor if you want - see AsciiLock.hs for what the characters mean.
+
+
+Rough description of the game mechanics
+=======================================
+* Locks consist of blocks, pivots and balls. Blocks can be connected to other
+    blocks by springs. Pivots have arms. When picking a lock, you control two
+    tools - a hook, which acts as a mobile one-armed pivot, and a wrench,
+    which acts as a mobile block with momentum.
+* To open a lock, the "bolthole" (the area in the top-right) must be empty.
+* Each turn, the tools push and rotate according to the player's commands, and
+    then each spring which is compressed or extended beyond its natural length
+    pushes/pulls on the block at its end.
+* If two forces try to move a piece in two different directions, or try to
+    move two pieces into the same hex, one or both is blocked.
+* If a pivot/hook is trying to turn and there's a piece in the way of one of
+    its arms, it will at first try to push the piece away - but if that force
+    is blocked, it will try instead to pull the piece round with the arm as it
+    turns.
+* If two springs are trying to push two blocks into the same hex, they will
+    generally both be blocked. However, if one of the forces is also pushing
+    against a fixed piece such as a tool, it won't block the other force.
+
+
+Full details of the metagame mechanics
+======================================
+(Where by the 'game' I mean the lock-picking bit, and by the 'metagame' I mean
+the bit with the 3-letter codenames and the three lock slots and notes and so
+on.)
+
+Scoring is always relative - each player has a score relative to each other
+player. That score is the number of the second player's lock slots to which
+the first player has access, minus the number of the first player's lock slots
+to which the second player has access.
+
+A player accesses a lock slot when one of the following holds
+    * the player has solved the lock in the slot, and declared the solution;
+    * the player has read three notes on the lock in the slot;
+    * there's no lock in the slot, and the player has accessed all the slots
+	which do have locks in them.
+
+A player reads every note in every lock the player accesses. When a lock is
+replaced ('retired'), each note secured by the lock becomes 'public', and is
+read by every player.
+
+Note this means that once three notes on a lock become public, every player
+accesses the lock. The lock is then 'public' (and its owner should replace
+it!).
+
+To declare a solution, you must secure a note on it behind a non-public lock
+in one of your three lock slots. Once the note is placed, it can't be moved.
+
+
+Full details of the game mechanics
+==================================
+Springs are directed; one of the blocks it is connected to is the 'root', the
+other the 'end'. A block is stationary if it is not the end of any spring. The
+directed graph whose nodes are the blocks and whose edges are the springs is
+required to be acyclic. A spring may also be rooted in a pivot.
+
+I will now attempt to describe in full excrutiating detail the game physics,
+i.e. the algorithm used to determine what happens on a turn. You shouldn't
+need to read this to play the game! Experimenting and turning on blockage
+annotations should be enough.
+
+The following description corresponds to the code in Physics.hs. The algorithm
+is the result of an extended process of experiment and iterated
+simplification; see notes/game in the source distribution if you're perverse
+enough to want to read a scattered stream-of-consciousness account of the
+process.
+
+Each turn is separated into two phases. In the first phase, the forces arising
+from the player's move are 'resolved', possibly resulting in some movement. In
+the second phase, forces arising from stretched or compressed springs are
+resolved, possibly resulting in some movement.
+
+Here we abuse physics terminology, and use the term 'force' to refer to either
+a directional force in the usual sense, which we call a 'push', or a
+rotational force, which we call a 'torque'. A force is always on a piece, in
+some direction. Only the obvious six hex directions and the two obvious
+rotational directions occur; the magnitude is always 1. 
+
+Player moves result in forces on the tools in the obvious way. A wrench which
+is moving gets a push in that direction each turn (sorry, Newton!). A spring
+which is not at its relaxed length results in a push on the end piece.
+
+To _resolve_ these initial forces:
+    * each initial force is 'propagated to a force group'
+    * if a group is 'inconsistent' with another group: if one of the group is
+	'dominated' by the other, it is 'blocked'; else both are blocked
+    * each force in each unblocked force group is 'applied'
+
+To _apply_ a push: move the piece in the given direction.
+To _apply_ a torque: rotate the piece in the given direction.
+
+To _propagate_ a force:
+    * If applying the force would result in an overlap with another piece,
+	push that piece - but if it's an arm, twist the pivot instead when
+	that makes sense.
+    * If the force is a torque and an arm is pushing on another piece, we have
+	a special rule: if the obvious push ends up propagating to a resisted
+	force (see below), then it is replaced with a 'clawing' push. e.g. in
+	the following situation:
+	    \ O #
+	     o
+	if the pivot is twisted clockwise, the ball will first be pushed east,
+	but when that force propagates to a push on the stationary block to
+	its east which is resisted, processing will back up and a push on the
+	ball southeast will be tried instead.
+    * A spring connecting two blocks transmits a push on one block to the
+	other unless the direction is such that the push is tending to
+	compress/extend the spring and the spring isn't already fully
+	compressed/extended.
+
+A force is _resisted_ if it is a push on a pivot or on a block which isn't the
+    end of a spring, or it's a torque on a block, or it's a force on a hook
+    which isn't already getting that force this phase as the result of a
+    player move, or it's a force on a wrench which isn't a push in the
+    direction it's already moving.
+
+To _propagate an initial force to a force group_:
+    * Propagate the force as above, then recursively propagate the resulting
+	forces, checking for resistance as we go. The force group consists of
+	all the resulting propagated forces.
+    * On resistance: back up to try clawing as described above if appropriate,
+	else consider the whole force group resisted - so the resulting force
+	group is empty.
+
+Two forces are _inconsistent_ when
+    * they act on the same piece in different directions, or
+    * applying both at once results in overlapping pieces.
+
+Two force groups are _inconsistent_ iff there is some force from the first
+    which is inconsistent with some force from the second.
+
+A force group _dominates_ another iff their initial forces are spring forces,
+    and the root of the first spring is an ancestor of the root of the second
+    spring, where an ancestor of a block B is recursively defined to be a block
+    which is the root of a spring which ends at B, or an ancestor of such a
+    block.
+
+-- mbays@sdf.org 2013
diff --git a/SDLRender.hs b/SDLRender.hs
new file mode 100644
--- /dev/null
+++ b/SDLRender.hs
@@ -0,0 +1,462 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module SDLRender where
+
+import Graphics.UI.SDL
+import Graphics.UI.SDL.Primitives
+import qualified Graphics.UI.SDL.TTF as TTF
+import Data.Monoid
+import Control.Monad
+import Control.Monad.IO.Class
+import Control.Monad.Trans.Reader
+import Control.Monad.Trans.Maybe
+import Control.Monad.Trans.Class
+import qualified Data.Map as Map
+import Data.List (maximumBy)
+import Data.Function (on)
+
+import Hex
+import GameState
+import GameStateTypes
+import BoardColouring
+import Physics
+import Command
+
+-- aaPolygon seems to be a bit buggy in sdl-gfx-0.6.0
+aaPolygon' surf verts col =
+    aaLines surf (verts ++ take 1 verts) col
+-- aaCircle too
+aaCircle' surf x y rad col =
+    if (rad == 1) then pixel surf x y col
+    else aaCircle surf x y rad col
+
+aaLines surf verts col =
+    sequence_ [ aaLine surf x y x' y' col |
+	((x,y),(x',y')) <- zip (take (length verts - 1) verts) (drop 1 verts) ]
+
+rimmedPolygon surf verts fillCol rimCol = do
+    filledPolygon surf verts fillCol
+    aaPolygon' surf verts $ opaquify rimCol
+    return ()
+
+rimmedCircle surf centre@(SVec x y) rad fillCol rimCol = do
+    filledCircle surf (fromIntegral x) (fromIntegral y) (fromIntegral rad) fillCol
+    aaCircle' surf (fromIntegral x) (fromIntegral y) (fromIntegral rad) $ opaquify rimCol
+    return ()
+
+type Glyph = SVec -> Int -> Surface -> IO ()
+
+ysize :: Int -> Int
+ysize size = round $ fromIntegral size / sqrt 3
+
+corner :: Integral i => SVec -> Int -> Int -> (i,i)
+corner (SVec x y) size hextant = (fromIntegral $ x+dx, fromIntegral $ y+dy)
+    where
+	[dx,dy] = f hextant
+	f 0 = [size, -ysize size]
+	f 1 = [0, -2*ysize size]
+	f 2 = [-size, -ysize size]
+	f n | n < 6 = let [x,y] = f (5-n) in [x,-y]
+	    | n < 0 = f (6-n)
+	    | otherwise = f (n`mod`6)
+
+innerCorner :: Integral i => SVec -> Int -> HexDir -> (i,i)
+innerCorner (SVec x y) size dir = (fromIntegral $ x+dx, fromIntegral $ y+dy)
+    where
+	[dx,dy] = f dir
+	f dir
+	    | dir == hu = [2*isize, 0]
+	    | dir == hv = [-isize, -ysize size]
+	    | dir == hw = [-isize, ysize size]
+	    | not (isHexDir dir) = error "innerCorner: not a hexdir"
+	    | otherwise = map (\z -> -z) $ f $ neg dir
+	isize = size `div` 3
+
+edge :: Integral i => SVec -> Int -> HexDir -> (i,i)
+edge (SVec x y) size dir = (fromIntegral $ x+dx, fromIntegral $ y+dy)
+    where
+	[dx,dy] = f dir
+	f dir
+	    | dir == hu = [size, 0]
+	    | dir == hv = [-size`div`2, -3*ysize size`div`2]
+	    | dir == hw = [-size`div`2, 3*ysize size`div`2]
+	    | not (isHexDir dir) = error "edge: not a hexdir"
+	    | otherwise = map (\z -> -z) $ f $ neg dir
+
+tileGlyph :: Tile -> Pixel -> Glyph
+
+tileGlyph (BlockTile adjs) col centre size surf =
+    rimmedPolygon surf corners col $ bright col
+	where 
+	    corners = concat [
+		if or $ map adjAt [0,1]
+		then [corner centre size $ hextant dir]
+		else if adjAt $ -1
+		    then []
+		    else [innerCorner centre size dir]
+		| dir <- hexDirs
+		, let adjAt r = rotate r dir `elem` adjs
+		]
+
+tileGlyph (SpringTile extn dir) col centre size surf =
+    aaLines surf points $ bright col
+	where
+	    n = 3*case extn of
+		Stretched -> 1
+		Relaxed -> 2
+		Compressed -> 3
+	    dir' = if dir == zero then hu else dir
+	    (sx,sy) = corner centre size (hextant dir' - 1)
+	    (offx,offy) = corner centre size (hextant dir')
+	    (ex,ey) = corner centre size (hextant dir' - 3)
+	    points = [ (x+dx,y+dy)
+		| i <- [0..n]
+		, i`mod`3 /= 1
+		, let (x,y) = if i`mod`3==0 then (sx,sy) else (offx,offy)
+		, let (dx,dy) = ((i*(ex-sx))`div`n, (i*(ey-sy))`div`n) ]
+
+tileGlyph (PivotTile dir) col centre size surf = do
+    rimmedCircle surf centre rad col $ bright col
+    when (dir /= zero)
+	$ void $ aaLine surf `uncurry` from `uncurry` to $ bright col
+    return ()
+	where
+	    rad = (7*size)`div`8
+	    from = edge centre rad $ neg dir
+	    to = edge centre rad dir
+
+tileGlyph (ArmTile dir _) col centre size surf =
+    void $ aaLine surf `uncurry` from `uncurry` to $ bright col
+	where
+	    dir' = if dir == zero then hu else dir
+	    from = edge centre size $ neg dir'
+	    to = innerCorner centre size dir'
+
+tileGlyph HookTile col centre size surf =
+    rimmedCircle surf centre rad col $ bright col
+	where
+	    rad = (7*size)`div`8
+
+tileGlyph (WrenchTile mom) col centre size surf = do
+    rimmedCircle surf centre (size`div`3) col $ bright col
+    when (mom /= zero) $
+	let 
+	    (fx,fy) = innerCorner centre size $ neg mom
+	    (tx,ty) = edge centre size $ neg mom
+	    shifts = map (map (`div` 2)) $
+		[ [ x1-x0, y1-y0 ]
+		| rot <- [-1,0,1]
+		, let (x0,y0) = innerCorner centre size $ neg mom
+		, let (x1,y1) = innerCorner centre size $ rotate rot $ neg mom
+		]
+	in sequence_
+	    [ aaLine surf (fx+dx) (fy+dy) (tx+dx) (ty+dy) $ col
+	    | [dx,dy] <- shifts ]
+
+tileGlyph BallTile col centre size surf =
+    rimmedCircle surf centre rad (faint col) (obscure col)
+    where rad = (7*size)`div`8
+
+blockedArm :: HexDir -> TorqueDir -> Pixel -> Glyph
+blockedArm armdir tdir col centre size surf =
+    void $ aaLine surf `uncurry` from `uncurry` to $ col
+	where
+	    from = innerCorner centre size $ rotate (2*tdir) armdir
+	    to = edge centre size $ rotate tdir armdir
+
+blockedBlock :: Tile -> HexDir -> Pixel -> Glyph
+blockedBlock tile dir col centre size =
+    tileGlyph tile col (shift <+> centre) size
+	where
+	    shift = SVec (x'-x) (y'-y)
+	    (x,y) = innerCorner centre size dir
+	    (x',y') = edge centre size dir
+
+blockedPush :: HexDir -> Pixel -> Glyph
+blockedPush dir col centre size surf = do
+{-
+    void $ rimmedPolygon surf verts (obscure col) (dim col)
+	where verts =
+		[ innerCorner centre size $ rotate 1 dir
+		, innerCorner centre size $ rotate (-1) dir
+		, innerCorner centre size dir ]
+-}
+    void $ aaLine surf `uncurry` base `uncurry` tip $ col
+    --void $ aaLine surf `uncurry` from' `uncurry` to' $ col
+    void $ aaLine surf `uncurry` tip `uncurry` (arms!!0) $ col
+    void $ aaLine surf `uncurry` tip `uncurry` (arms!!1) $ col
+    where
+	--base@(bx,by) = innerCorner centre size dir
+	SVec bx' by' = centre
+	base@(bx,by) = (fromIntegral bx',fromIntegral by')
+	tip@(tx,hy) = edge centre size dir
+	arms = [(bx + (tx-bx)`div`2 + dir*(hy-by)`div`4,
+	    by + (hy-by)`div`2 - dir*(tx-bx)`div`4) | dir <- [-1,1]]
+
+	--from' = corner centre size $ hextant dir
+	--to' = corner centre size $ (hextant dir) - 1
+	
+collisionMarker :: Glyph
+collisionMarker centre size surf =
+    rimmedCircle surf centre (size`div`3) (bright orange) $ bright orange
+
+hollowGlyph :: Pixel -> Glyph
+hollowGlyph col centre size surf =
+    aaPolygon' surf corners $ opaquify col
+    where corners = map (corner centre size) [0..5]
+hollowInnerGlyph col centre size surf =
+    aaPolygon' surf corners $ opaquify col
+    where corners = [ innerCorner centre size dir | dir <- hexDirs ]
+
+filledHexGlyph :: Pixel -> Glyph
+filledHexGlyph col centre size surf =
+    rimmedPolygon surf corners col $ brightish col
+    where corners = map (corner centre size) [0..5]
+
+buttonGlyph :: Pixel -> Glyph
+buttonGlyph = tileGlyph (BlockTile [])
+
+useFiveColourButton :: Bool -> Glyph
+useFiveColourButton using centre size surf = do
+    mapM_ (\h -> tileGlyph (BlockTile [])
+	    (dim $ colourWheel (if using then h`div`2 else 1))
+	    (SVec `uncurry` corner centre (size`div`2) h) (size`div`2) surf)
+	[0,2,4]
+
+showBlocksButton :: Bool -> Glyph
+showBlocksButton showing centre size surf = do
+    tileGlyph (BlockTile []) (dim red) centre size surf
+    when showing $ blockedPush hu (bright orange) centre size surf
+
+whsButtonsButton :: WrHoSel -> Glyph
+whsButtonsButton whs centre size surf = do
+    when (whs /= WHSHook) $
+	tileGlyph (WrenchTile zero) col (miniCentre 0) miniSize surf
+    when (whs /= WHSWrench) $ do
+	tileGlyph HookTile col (miniCentre 4) miniSize surf
+	tileGlyph (ArmTile hv False) col (miniCentre 2) miniSize surf
+    where
+	miniSize = size `div` 2
+	miniCentre h = SVec `uncurry` corner centre miniSize h
+	col = dim white
+
+cursor :: Glyph
+cursor = hollowGlyph $ bright white
+
+unfreshGlyph centre@(SVec x y) size surf = do
+    let col = bright red
+    hollowInnerGlyph col centre size surf
+    sequence_ [pixel surf (fromIntegral $ x+(i*size`div`4)) (fromIntegral y) col
+	| i <- [-1..1] ]
+
+playerGlyph col centre size surf = filledHexGlyph col centre size surf
+
+setPixelAlpha alpha (Pixel v) = Pixel $ v `div` 0x100 * 0x100 + alpha
+bright = setPixelAlpha 0xff
+brightish = setPixelAlpha 0xc0
+dim = setPixelAlpha 0xa0
+obscure = setPixelAlpha 0x80
+faint = setPixelAlpha 0x40
+invisible = setPixelAlpha 0x00
+
+pixelToRGBA (Pixel v) =
+    let (r,v') = divMod v 0x1000000
+	(g,v'') = divMod v' 0x10000
+	(b,a) = divMod v'' 0x100
+    in (r,g,b,a)
+rgbaToPixel (r,g,b,a) = Pixel $ a+0x100*(b+0x100*(g+0x100*r))
+opaquify p =
+    let (r,g,b,a) = pixelToRGBA p
+	[r',g',b'] = map (\v -> (v*a)`div`0xff) [r,g,b]
+    in rgbaToPixel (r',g',b',0xff)
+
+black = Pixel 0x01000000
+white = Pixel 0xffffff00
+orange = Pixel 0xff7f0000
+
+colourWheel :: Int -> Pixel
+colourWheel n = Pixel $ (((((r * 0x100) + g) * 0x100) + b) * 0x100) + a
+    where [r,g,b] = map (\on -> if on then 0xff else 0) $ colourWheel' n
+	  a = 0x00
+	  colourWheel' 0 = [True, False, False]
+	  colourWheel' 1 = [True, True, False]
+	  colourWheel' n = let [a,b,c] = colourWheel' $ n-2 in [c,a,b]
+
+red = colourWheel 0
+yellow = colourWheel 1
+green = colourWheel 2
+cyan = colourWheel 3
+blue = colourWheel 4
+purple = colourWheel 5
+
+colourOf colouring idx = 
+    case Map.lookup idx colouring of
+	    Nothing -> white
+	    Just n -> colourWheel n
+
+ownedTileGlyph :: PieceColouring -> [PieceIdx] -> OwnedTile -> Glyph
+ownedTileGlyph colouring highlight (owner,t) =
+    let col = colourOf colouring owner
+    in tileGlyph t $ (if owner `elem` highlight then bright else dim) col
+
+data SVec = SVec { cx, cy :: Int }
+    deriving (Eq, Ord, Show)
+instance Monoid SVec where
+    mempty = SVec 0 0
+    mappend (SVec x y) (SVec x' y') = SVec (x+x') (y+y') 
+instance Grp SVec where
+    neg (SVec x y) = SVec (-x) (-y)
+type CCoord = PHS SVec
+
+hexVec2SVec :: Int -> HexVec -> SVec
+hexVec2SVec size (HexVec x y z) =
+    SVec ((x-z) * size) (-y * 3 * ysize size)
+
+sVec2dHV :: Int -> SVec -> (Double,Double,Double)
+sVec2dHV size (SVec sx sy) =
+    let sx',sy',size' :: Double
+	[sx',sy',size',ysize'] = map fromIntegral [sx,sy,size,ysize size]
+	y' = -sy' / ysize' / 3
+	x' = ((sx' / size') - y') / 2
+	z' = -((sx' / size') + y') / 2
+    in (x',y',z')
+
+sVec2HexVec :: Int -> SVec -> HexVec
+sVec2HexVec size sv =
+    let (x',y',z') = sVec2dHV size sv
+	unrounded = Map.fromList [(1,x'),(2,y'),(3,z')]
+	rounded = Map.map round unrounded
+	maxdiff = fst $ maximumBy (compare `on` snd) $
+	    [ (i, abs $ c'-c) | i <- [1..3],
+		let c' = unrounded Map.! i, let c = fromIntegral $ rounded Map.! i]
+	[x,y,z] = map snd $ Map.toList $
+	    Map.adjust (\x -> x - (sum $ Map.elems rounded)) maxdiff rounded
+    in HexVec x y z
+
+data RenderContext = RenderContext
+    { renderSurf :: Surface
+    , renderBGSurf :: Maybe Surface
+    , renderHCentre :: HexPos
+    , renderSCentre :: SVec
+    , renderSize :: Int
+    , renderFont :: Maybe TTF.Font
+    }
+type RenderM = ReaderT RenderContext IO
+
+displaceRender :: SVec -> RenderM a -> RenderM a
+displaceRender disp = local displace
+    where displace rc = rc { renderSCentre = renderSCentre rc <+> disp }
+
+recentreAt :: HexVec -> RenderM a -> RenderM a
+recentreAt v m = do
+    size <- asks renderSize
+    displaceRender (hexVec2SVec size v) m
+
+rescaleRender :: RealFrac n => n -> RenderM a -> RenderM a
+rescaleRender r = local resize
+    where resize rc = rc { renderSize = round $ r * (fromIntegral $ renderSize rc) }
+
+withFont :: Maybe TTF.Font -> RenderM a -> RenderM a
+withFont font = local refont
+    where refont rc = rc { renderFont = font }
+
+erase :: RenderM ()
+erase = fillRectBG Nothing
+
+fillRectBG :: Maybe Rect -> RenderM ()
+fillRectBG mrect = do
+    surf <- asks renderSurf
+    mbgsurf <- asks renderBGSurf
+    void $ liftIO $ maybe
+	(fillRect surf mrect black)
+	(\bgsurf -> blitSurface bgsurf mrect surf mrect)
+	mbgsurf
+
+drawBasicBG :: Int -> RenderM ()
+drawBasicBG maxR = sequence_ [ drawAtRel (hollowGlyph $ colAt v) v | v <- hexDisc maxR ]
+    where
+	colAt v@(HexVec hx hy hz) = let
+		[r,g,b] = map (\h -> fromIntegral $ ((0xff*)$ abs h)`div`maxR) [hx,hy,hz]
+		a = fromIntegral $ (0xb0 * (maxR - abs (hexLen v)))`div`maxR
+	    in rgbaToPixel (r,g,b,a)
+
+drawAt :: Glyph -> HexPos -> RenderM ()
+drawAt gl pos = do
+	centre <- asks renderHCentre
+	drawAtRel gl (pos <-> centre)
+drawAtRel gl v = do
+	(surf, scrCentre, size) <- asks $ liftM3 (,,) renderSurf renderSCentre renderSize
+	let cpos = scrCentre <+> (hexVec2SVec size v)
+	liftIO $ gl cpos size surf
+
+messageCol = white
+accessedCol = green
+dimWhiteCol = Pixel 0xa0a0a000
+buttonTextCol = white
+errorCol = red
+
+pixelToColor p =
+    let (r,g,b,_) = pixelToRGBA p
+    in Color (fromIntegral r) (fromIntegral g) (fromIntegral b)
+
+renderStrColAtLeft = renderStrColAt' False
+renderStrColAt = renderStrColAt' True
+renderStrColAt' :: Bool -> Pixel -> String -> HexVec -> RenderM ()
+renderStrColAt' centred c str v = void $ runMaybeT $ do
+    font <- MaybeT $ asks renderFont
+    fsurf <- MaybeT $ liftIO $ TTF.tryRenderTextBlended font str $ pixelToColor c
+    (surf, scrCentre, size) <- lift $ asks $
+	liftM3 (,,) renderSurf renderSCentre renderSize
+    let SVec x y = scrCentre <+> (hexVec2SVec size v)
+	    <+> neg (SVec 0 ((surfaceGetHeight fsurf-1)`div`2) <+>
+		if centred
+		    then SVec ((surfaceGetWidth fsurf)`div`2) 0
+		    else SVec 0 0)
+    void $ liftIO $ blitSurface fsurf Nothing surf (Just $ Rect x y 0 0)
+
+blankRow v = do
+    (surf, scrCentre, size) <- asks $
+	liftM3 (,,) renderSurf renderSCentre renderSize
+    let SVec _ y = scrCentre <+> (hexVec2SVec size v)
+	w = surfaceGetWidth surf
+	h = ceiling $ fromIntegral size * 2 * sqrt 3
+    fillRectBG $ Just $ Rect 0 (y-h`div`2) w h
+
+drawCursorAt :: Maybe HexPos -> RenderM ()
+drawCursorAt (Just pos) = drawAt cursor pos
+drawCursorAt _ = return ()
+
+drawBlocked :: GameState -> PieceColouring -> Bool -> Force -> RenderM ()
+drawBlocked st colouring blocking (Torque idx dir) = do
+    let (pos,arms) = case getpp st idx of
+	    PlacedPiece pos (Pivot arms) -> (pos,arms)
+	    PlacedPiece pos (Hook arm _) -> (pos,[arm])
+	    _ -> (pos,[])
+	col = if blocking then bright $ purple else dim $ colourOf colouring idx
+    sequence_ [ drawAt (blockedArm arm dir col) (arm <+> pos) |
+	arm <- arms ]
+drawBlocked st colouring blocking (Push idx dir) = do
+    let footprint = plPieceFootprint $ getpp st idx
+	fullfootprint = fullFootprint st idx
+	col = bright $ if blocking then purple else orange
+    sequence_ [ drawAt (blockedPush dir col) pos
+	| pos <- footprint
+	, (dir<+>pos) `notElem` fullfootprint ]
+    -- drawAt (blockedPush dir $ bright orange) $ placedPos $ getpp st idx
+
+blitAt :: Surface -> HexVec -> RenderM ()
+blitAt surface v = do
+    (surf, scrCentre, size) <- asks $ liftM3 (,,) renderSurf renderSCentre renderSize
+    let SVec x y = scrCentre <+> (hexVec2SVec size v)
+	w = surfaceGetWidth surface
+	h = surfaceGetHeight surface
+    void $ liftIO $ blitSurface surface Nothing surf $ Just $
+	Rect (x-w`div`2) (y-h`div`2) (w+1) (h+1)
diff --git a/SDLUI.hs b/SDLUI.hs
new file mode 100644
--- /dev/null
+++ b/SDLUI.hs
@@ -0,0 +1,520 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module SDLUI where
+
+import Graphics.UI.SDL hiding (flip)
+import qualified Graphics.UI.SDL as SDL
+import qualified Graphics.UI.SDL.TTF as TTF
+import Control.Concurrent.STM
+import Control.Applicative hiding ((<*>))
+import qualified Data.Map as Map
+import Data.Map (Map)
+import Data.Maybe
+import Control.Monad.State
+import Control.Monad.Trans.Maybe
+import Control.Monad.Trans.Reader
+import Data.Word
+import Data.Array
+import Data.List
+import Data.Ratio
+import Data.Function (on)
+--import Debug.Trace (traceShow)
+
+import Hex
+import Command
+import GameState (stateBoard)
+import GameStateTypes
+import BoardColouring
+import Lock
+import Physics
+import KeyBindings
+import Mundanities
+import Metagame
+import SDLRender
+import InputMode
+import Maxlocksize
+
+data UIState = UIState { scrHeight::Int, scrWidth::Int
+    , gsSurface::Maybe Surface
+    , bgSurface::Maybe Surface
+    , lastDrawArgs::Maybe DrawArgs
+    , miniLocks::Map Lock Surface
+    , metaSelectables::Map HexVec Selectable
+    , contextButtons::[ButtonGroup]
+    , uiOptions::UIOptions
+    , settingBinding::Maybe Command
+    , uiKeyBindings :: Map InputMode KeyBindings
+    , dispFont::Maybe TTF.Font
+    , dispFontSmall::Maybe TTF.Font
+    , lastFrameTicks::Word32
+    , paintTileIndex::Int
+    , leftButtonDown::Maybe HexVec, middleButtonDown::Maybe HexVec, rightButtonDown::Maybe HexVec
+    , mousePos::(HexVec,Bool)
+    , message::Maybe (Pixel, String)
+    , hoverStr :: Maybe String
+    , dispCentre::HexPos
+    , dispLastCol::PieceColouring }
+    deriving (Eq, Ord, Show)
+type UIM = StateT UIState IO
+nullUIState = UIState 0 0 Nothing Nothing Nothing Map.empty Map.empty []
+    defaultUIOptions Nothing Map.empty Nothing Nothing 0 0 Nothing Nothing Nothing
+    (zero,False) Nothing Nothing (PHS zero) Map.empty
+
+data UIOptions = UIOptions
+    { useFiveColouring::Bool
+    , showBlocks::Bool
+    , whsButtons::WrHoSel
+    , useBackground::Bool
+    }
+    deriving (Eq, Ord, Show, Read)
+defaultUIOptions = UIOptions False False WHSSelected True
+modifyUIOptions f = modify $ \s -> s { uiOptions = f $ uiOptions s }
+
+renderToMain :: RenderM a -> UIM a
+renderToMain m = do
+    surf <- liftIO getVideoSurface
+    renderToMainWithSurf surf m
+renderToMainWithSurf :: Surface -> RenderM a -> UIM a
+renderToMainWithSurf surf m = do
+    (scrCentre, size) <- getGeom
+    centre <- gets dispCentre
+    mfont <- gets dispFont
+    bgsurf <- gets bgSurface
+    liftIO $ runReaderT m $ RenderContext surf bgsurf centre scrCentre size mfont
+
+
+refresh :: UIM ()
+refresh = do 
+    surface <- liftIO getVideoSurface
+    liftIO $ SDL.flip surface
+
+waitFrame :: UIM ()
+waitFrame = do
+    last <- gets lastFrameTicks
+    let next = last + 1000 `div` 30
+    now <- liftIO getTicks
+    -- liftIO $ print now
+    when (now < next) $
+	liftIO $ delay (next - now)
+    modify $ \ds -> ds { lastFrameTicks = now }
+
+
+data Button = Button { buttonPos::HexVec, buttonCmd::Command }
+    deriving (Eq, Ord, Show)
+type ButtonGroup = ([Button],(Int,Int))
+singleButton :: HexVec -> Command -> Int -> ButtonGroup
+singleButton pos cmd col = ([Button pos cmd], (col,0))
+getButtons :: InputMode -> UIM [ ButtonGroup ]
+getButtons mode = do
+    whs <- gets $ whsButtons.uiOptions
+    cntxtButtons <- gets contextButtons
+    return $ cntxtButtons ++ case mode of 
+	IMEdit -> [
+	    singleButton (tl<+>hv) CmdTest 4
+	    , singleButton (tl<+>(neg hw)) CmdPlay 2
+	    , markGroup
+	    , ([quitButton, Button (br<+>2<*>hu) CmdWriteState],(0,2))
+	    , ( [ Button (bl<+>dir) (CmdDir WHSSelected dir) | dir <- hexDirs ] ++
+		[ Button (bl<+>((-2)<*>hv)) (CmdRotate (-1))
+		, Button (bl<+>((-2)<*>hw)) (CmdRotate 1) ], (4,0) )
+	    , ( [ Button bl CmdSelect ], (0,0))
+	    , ([Button (paintButtonStart <+> hu <+> i<*>hv) (paintTileCmds!!i)
+		| i <- take (length paintTiles) [0..] ],(5,0))
+	    ]
+	IMPlay -> 
+	    [ ([quitButton],(0,2))
+	    , markGroup
+	    , ( [ Button bl CmdWait ], (0,0))
+	    , ( [ Button (bl<+>dir) (CmdDir whs dir) | dir <- hexDirs ], (4,0) )
+	    ] ++
+	    (if whs == WHSWrench then [] else
+		[ ( [ Button (bl<+>((-2)<*>hv)) (CmdRotate (-1))
+		    , Button (bl<+>((-2)<*>hw)) (CmdRotate 1)
+		    ], (4,0) )
+	    ]) ++
+	    (if whs /= WHSSelected then [] else
+		[ ( [ Button (bl<+>(2<*>hv)<+>hw) (CmdTile $ HookTile)
+		    , Button (bl<+>(2<*>hv)) (CmdTile $ WrenchTile zero)
+		    ], (2,0) )
+	    ]) ++
+	    [ singleButton tr CmdOpen 1 ]
+	IMReplay -> [ ([quitButton],(0,2))
+	    , markGroup ]
+	IMMeta ->
+	    [ ([quitButton],(0,2))
+	    , singleButton serverPos CmdSetServer 2
+	    , singleButton (serverPos<+>neg hu) CmdToggleCacheOnly 0
+	    , singleButton lockLinePos CmdSelectLock 4
+	    , singleButton (miniLockPos <+> 2<*>hv <+> neg hu <+> hw) CmdEdit 2
+	    , singleButton (codenamePos <+> neg hu) (CmdSelCodename Nothing) 2
+	    , singleButton (serverPos <+> 2<*>neg hv <+> hw) CmdTutorials 1
+	    ]
+		
+	_ -> []
+	where
+	    quitButton = Button br CmdQuit
+	    markGroup = ([Button (tl<+>hw) CmdMark, Button (tl<+>hw<+>neg hu) CmdJumpMark],(0,0))
+	    tr = periphery 0
+	    tl = periphery 2
+	    bl = periphery 3
+	    br = periphery 5
+
+data Selectable = SelOurLock
+    | SelLock ActiveLock
+    | SelLockUnset ActiveLock
+    | SelSelectedCodeName
+    | SelOurAL
+    | SelUndeclared Undeclared
+    | SelReadNote NoteInfo
+    | SelSolution NoteInfo
+    | SelAccessed Codename
+    | SelRandom Codename
+    | SelSecured NoteInfo
+    | SelOldLock LockSpec
+    deriving (Eq, Ord, Show)
+
+registerSelectable v r s =
+    modify $ \ds -> ds {metaSelectables = foldr
+	(`Map.insert` s) (metaSelectables ds) $ map (v<+>) $ hexDisc r}
+registerButtonGroup g = modify $ \ds -> ds {contextButtons = g:contextButtons ds}
+clearSelectables,clearButtons :: UIM ()
+clearSelectables = modify $ \ds -> ds {metaSelectables = Map.empty}
+clearButtons = modify $ \ds -> ds {contextButtons = []}
+
+registerUndoButtons noUndo noRedo = do
+    unless noUndo $ registerButtonGroup $ singleButton (periphery 2) CmdUndo 0
+    unless noRedo $ registerButtonGroup $ singleButton (periphery 2<+>neg hu) CmdRedo 2
+
+commandOfSelectable IMMeta SelOurLock _ = CmdEdit
+commandOfSelectable IMMeta (SelLock (ActiveLock _ i)) False = CmdSolve (Just i)
+commandOfSelectable IMMeta (SelLock (ActiveLock _ i)) True = CmdPlaceLock (Just i)
+commandOfSelectable IMMeta (SelLockUnset (ActiveLock _ i)) _ = CmdPlaceLock (Just i)
+commandOfSelectable IMMeta SelSelectedCodeName False = CmdSelCodename Nothing
+commandOfSelectable IMMeta SelSelectedCodeName True = CmdHome
+commandOfSelectable IMMeta SelOurAL _ = CmdHome
+commandOfSelectable IMMeta (SelUndeclared undecl) _ = CmdDeclare $ Just undecl
+commandOfSelectable IMMeta (SelReadNote note) False = CmdSelCodename $ Just $ noteAuthor note
+commandOfSelectable IMMeta (SelReadNote note) True = CmdViewSolution $ Just note
+commandOfSelectable IMMeta (SelSolution note) False = CmdSelCodename $ Just $ noteAuthor note
+commandOfSelectable IMMeta (SelSolution note) True = CmdViewSolution $ Just note
+commandOfSelectable IMMeta (SelAccessed name) _ = CmdSelCodename $ Just name
+commandOfSelectable IMMeta (SelRandom name) _ = CmdSelCodename $ Just name
+commandOfSelectable IMMeta (SelSecured note) False = CmdSelCodename $ Just $ lockOwner $ noteOn note
+commandOfSelectable IMMeta (SelSecured note) True = CmdViewSolution $ Just note
+commandOfSelectable IMMeta (SelOldLock ls) _ = CmdPlayLockSpec $ Just ls
+commandOfSelectable IMTextInput (SelLock (ActiveLock _ i)) _ = CmdInputSelLock i
+commandOfSelectable IMTextInput (SelLockUnset (ActiveLock _ i)) _ = CmdInputSelLock i
+commandOfSelectable IMTextInput (SelReadNote note) _ = CmdInputCodename $ noteAuthor note
+commandOfSelectable IMTextInput (SelSolution note) _ = CmdInputCodename $ noteAuthor note
+commandOfSelectable IMTextInput (SelSecured note) _ = CmdInputCodename $ lockOwner $ noteOn note
+commandOfSelectable IMTextInput (SelRandom name) _ = CmdInputCodename name
+commandOfSelectable IMTextInput (SelUndeclared undecl) _ = CmdInputSelUndecl undecl
+commandOfSelectable _ _ _ = CmdNone
+
+cmdAtMousePos pos@(mPos,central) im selMode = do
+    buttons <- (concat . map fst) <$> getButtons im
+    sels <- gets metaSelectables
+    return $ listToMaybe $
+	[ buttonCmd button
+	    | button <- buttons, mPos == buttonPos button, central]
+	++ maybe [] (\isRight -> [ commandOfSelectable im sel isRight
+	    | Just sel <- [Map.lookup mPos sels] ]) selMode
+
+data UIOptButton a = UIOptButton { getUIOpt::UIOptions->a, setUIOpt::a->UIOptions->UIOptions,
+    uiOptVals::[a], uiOptPos::HexVec, uiOptGlyph::a->Glyph, uiOptDescr::a->String }
+
+-- non-uniform type, so can't use a list...
+uiOB1 = UIOptButton useFiveColouring (\v o -> o {useFiveColouring=v}) [True,False]
+	(periphery 0 <+> 2 <*> neg hw <+> neg hv) useFiveColourButton
+	(\v -> if v then "Adjacent pieces get different colours" else
+	"Pieces are coloured according to type")
+uiOB2 = UIOptButton showBlocks (\v o -> o {showBlocks=v}) [True,False]
+	(periphery 0 <+> 2 <*> neg hw <+> 3 <*> neg hv) showBlocksButton
+	(\v -> if v then "Blocked and blocking forces are annotated" else
+	"Blockage annotations disabled")
+uiOB3 = UIOptButton whsButtons (\v o -> o {whsButtons=v}) [WHSSelected, WHSWrench, WHSHook]
+	(periphery 3 <+> 3 <*> hv) whsButtonsButton
+	(\v -> "Showing buttons for controlling " ++ case v of
+	    WHSSelected -> "selected tool"
+	    WHSWrench -> "wrench"
+	    WHSHook -> "hook")
+
+drawUIOptionButtons :: InputMode -> UIM ()
+drawUIOptionButtons mode = when (mode `elem` [IMPlay, IMEdit]) $ do
+    drawUIOptionButton uiOB1
+    drawUIOptionButton uiOB2 
+    when (mode == IMPlay) $ drawUIOptionButton uiOB3
+drawUIOptionButton b = do
+    value <- gets $ (getUIOpt b).uiOptions
+    renderToMain $ mapM_ (\g -> drawAtRel g (uiOptPos b))
+	[hollowGlyph $ obscure purple, uiOptGlyph b value]
+describeUIOptionButton b = do
+    value <- gets $ (getUIOpt b).uiOptions
+    return $ uiOptDescr b value
+-- XXX: hand-hacking lenses...
+toggleUIOption (UIOptButton getopt setopt vals _ _ _) = do
+    value <- gets $ getopt.uiOptions
+    let value' = head $ drop (1 + (fromMaybe 0 $ elemIndex value vals)) $ cycle vals
+    modifyUIOptions $ setopt value'
+
+readUIConfigFile :: UIM ()
+readUIConfigFile = do 
+    path <- liftIO $ confFilePath "SDLUI.conf"
+    mOpts <- liftIO $ readReadFile path
+    case mOpts of
+	Just opts -> modify $ \s -> s {uiOptions = opts}
+	Nothing -> return ()
+writeUIConfigFile :: UIM ()
+writeUIConfigFile = do
+    path <- liftIO $ confFilePath "SDLUI.conf"
+    opts <- gets uiOptions
+    liftIO makeConfDir
+    liftIO $ writeFile path $ show opts
+
+readBindings :: UIM ()
+readBindings = do 
+    path <- liftIO $ confFilePath "bindings"
+    mbdgs <- liftIO $ readReadFile path
+    case mbdgs of
+	Just bdgs -> modify $ \s -> s {uiKeyBindings = bdgs}
+	Nothing -> return ()
+writeBindings :: UIM ()
+writeBindings = do
+    path <- liftIO $ confFilePath "bindings"
+    bdgs <- gets uiKeyBindings
+    liftIO makeConfDir
+    liftIO $ writeFile path $ show bdgs
+
+paintTiles :: [ Maybe Tile ]
+paintTiles =
+    [ Just BallTile
+    , Just $ ArmTile zero False
+    , Just $ PivotTile zero
+    , Just $ SpringTile Relaxed zero
+    , Just $ BlockTile []
+    , Nothing
+    ]
+
+paintTileCmds = map (maybe CmdWait CmdTile) paintTiles
+
+getEffPaintTileIndex :: UIM Int
+getEffPaintTileIndex = do
+    mods <- liftIO getModState
+    if any (`elem` mods) [KeyModLeftCtrl, KeyModRightCtrl]
+    then return $ length paintTiles - 1
+    else gets paintTileIndex
+
+paintButtonStart :: HexVec
+paintButtonStart = periphery 0 <+> (- length paintTiles `div` 2)<*>hv
+
+drawPaintButtons :: UIM ()
+drawPaintButtons = do
+    pti <- getEffPaintTileIndex
+    renderToMain $ sequence_ [ 
+	do
+	    let gl = case paintTiles!!i of
+		    Nothing -> hollowInnerGlyph $ dim purple
+		    Just t -> tileGlyph t $ dim purple
+	    drawAtRel gl pos
+	    when selected $ drawAtRel cursor pos
+	| i <- take (length paintTiles) [0..]
+	, let pos = paintButtonStart <+> i<*>hv
+	, let selected = i == pti
+	]
+
+periphery 0 = ((3*maxlocksize)`div`2)<*>hu <+> ((3*maxlocksize)`div`4)<*>hv
+periphery n = rotate n $ periphery 0
+-- ^ XXX only peripheries 0,2,3,5 are guaranteed to be on-screen!
+--messageLineStart = (maxlocksize+1)<*>hw
+messageLineCentre = ((maxlocksize+1)`div`2)<*>hw <+> ((maxlocksize+1+1)`div`2)<*>neg hv
+titlePos = (maxlocksize+1)<*>hv <+> ((maxlocksize+1)`div`2)<*>hu
+
+screenWidthHexes,screenHeightHexes::Int
+screenWidthHexes = 32
+screenHeightHexes = 25
+getGeom :: UIM (SVec, Int)
+getGeom = do 
+    h <- gets scrHeight 
+    w <- gets scrWidth
+    let scrCentre = SVec (w`div`2) (h`div`2)
+    -- |size is the greatest integer such that
+    -- and [2*size*screenWidthHexes <= width
+    --	, 3*ysize size*screenHeightHexes <= height]
+    --	where ysize size = round $ fromIntegral size / sqrt 3
+    let size = max 1 $ minimum [ w`div`(2*screenWidthHexes)
+	    , floor $ sqrt 3 * (0.5 + (fromIntegral $ h`div`(3*screenHeightHexes)))]
+    return (scrCentre, size)
+
+data DrawArgs = DrawArgs [PieceIdx] Bool [Alert] GameState UIOptions
+    deriving (Eq, Ord, Show)
+
+drawMainGameState :: [PieceIdx] -> Bool -> [Alert] -> GameState -> UIM ()
+drawMainGameState highlight colourFixed alerts st = do
+    uiopts <- gets uiOptions
+    drawMainGameState' $ DrawArgs highlight colourFixed alerts st uiopts
+
+drawMainGameState' :: DrawArgs -> UIM ()
+drawMainGameState' args@(DrawArgs highlight colourFixed alerts st uiopts) = do
+    lastArgs <- gets lastDrawArgs
+    void $ if lastArgs == Just args
+	then do
+	    vidSurf <- liftIO getVideoSurface
+	    gsSurf <- liftM fromJust $ gets gsSurface
+	    liftIO $ blitSurface gsSurf Nothing vidSurf Nothing
+	else do
+	    modify $ \ds -> ds { lastDrawArgs = Just args }
+	    let board = stateBoard st
+	    lastCol <- gets dispLastCol
+	    let coloured = colouredPieces colourFixed st
+	    let colouring = if useFiveColouring uiopts
+		then boardColouring st coloured lastCol
+		else pieceTypeColouring st coloured
+	    modify $ \ds -> ds { dispLastCol = colouring }
+	    gsSurf <- liftM fromJust $ gets gsSurface
+	    renderToMainWithSurf gsSurf $ do
+		erase
+		sequence_ [ drawAt glyph pos | 
+		    (pos,glyph) <- Map.toList $ fmap (ownedTileGlyph colouring highlight) board
+		    ]
+
+		when (showBlocks uiopts) $ sequence_
+		    $ [ drawBlocked st colouring False force |
+		    AlertBlockedForce force <- alerts ]
+		    ++ [ drawBlocked st colouring True force |
+		    AlertBlockingForce force <- alerts ]
+		    ++ [ drawBlocked st colouring True force |
+		    AlertResistedForce force <- alerts ]
+		    -- ++ [ drawAt collisionMarker pos | AlertCollision pos <- alerts ]
+	    vidSurf <- liftIO getVideoSurface
+	    liftIO $ blitSurface gsSurf Nothing vidSurf Nothing
+
+drawMiniLock :: Lock -> HexVec -> UIM ()
+drawMiniLock lock v = do
+    surface <- Map.lookup lock <$> gets miniLocks >>= maybe new return
+    renderToMain $ blitAt surface v
+    where
+	miniLocksize = 3
+	new = do
+	    (_, size) <- getGeom
+	    let minisize = size `div` (ceiling $ lockSize lock % miniLocksize)
+	    let width = size*2*(miniLocksize*2+1)
+	    let height = ceiling $ fromIntegral size * sqrt 3 * fromIntegral (miniLocksize*2+1+1)
+	    surf <- liftIO $ createRGBSurface [] width height 16 0 0 0 0
+	    liftIO $ setColorKey surf [SrcColorKey] $ Pixel 0
+	    uiopts <- gets uiOptions
+	    let st = snd $ reframe lock
+		coloured = colouredPieces False st
+		colouring = if useFiveColouring uiopts
+		    then boardColouring st coloured Map.empty
+		    else pieceTypeColouring st coloured
+		draw = sequence_ [ drawAt glyph pos | 
+		    (pos,glyph) <- Map.toList $ fmap (ownedTileGlyph colouring []) $ stateBoard st ]
+	    liftIO $ runReaderT draw $
+		RenderContext surf Nothing (PHS zero) (SVec (width`div`2) (height`div`2)) minisize Nothing
+	    clearOldMiniLocks
+	    modify $ \ds -> ds { miniLocks = Map.insert lock surf $ miniLocks ds }
+	    return surf
+
+-- | TODO: do this more cleverly
+clearOldMiniLocks =
+    (>=50).Map.size <$> gets miniLocks >>= flip when clearMiniLocks
+clearMiniLocks = modify $ \ds -> ds { miniLocks = Map.empty}
+
+drawEmptyMiniLock v =
+    renderToMain $ recentreAt v $ rescaleRender 6 $ drawAtRel (hollowInnerGlyph $ dim white) zero
+
+getBindingStr :: InputMode -> UIM (Command -> String)
+getBindingStr mode = do
+    setting <- gets settingBinding
+    uibdgs <- Map.findWithDefault [] mode `liftM` gets uiKeyBindings
+    return (\cmd ->
+	if Just cmd == setting then "??"
+	else maybe "" showKey $ findBinding (uibdgs ++ bindings mode) cmd)
+
+drawButtons :: InputMode -> UIM ()
+drawButtons mode = do
+    buttons <- getButtons mode
+    bindingStr <- getBindingStr mode
+    renderToMain $ sequence_ $ concat [
+	    [ drawAtRel (buttonGlyph col) v >> renderStrColAt buttonTextCol bdg v |
+		(i,(v,bdg)) <- enumerate $ map (\b->(buttonPos b, bindingStr $ buttonCmd b)) $ buttonGroup
+		, let col = dim $ colourWheel (base+inc*i) ]
+	| (buttonGroup,(base,inc)) <- buttons
+	]
+    where enumerate = zip [0..]
+
+initVideo :: Int -> Int -> UIM ()
+initVideo w h = do
+    liftIO $ setVideoMode w h 0 [Resizable]
+    
+    -- see what size we actually got:
+    vinfo <- liftIO $ getVideoInfo
+    let [w',h'] = map ($vinfo) [videoInfoWidth,videoInfoHeight]
+
+    modify $ \ds -> ds { scrWidth = w' }
+    modify $ \ds -> ds { scrHeight = h' }
+    gssurf <- liftIO $ createRGBSurface [] w' h' 16 0 0 0 0
+    modify $ \ds -> ds { gsSurface = Just gssurf, lastDrawArgs = Nothing }
+
+    (_,size) <- getGeom
+    let fontfn = "VeraMono.ttf"
+    fontpath <- liftIO $ getDataPath fontfn
+    font <- liftIO $ TTF.tryOpenFont fontpath size
+    smallFont <- liftIO $ TTF.tryOpenFont fontpath (2*size`div`3)
+    modify $ \ds -> ds { dispFont = font, dispFontSmall = smallFont }
+
+    useBG <- gets $ useBackground.uiOptions
+    mbg <- if useBG then do
+	    bgsurf <- liftIO $ createRGBSurface [] w' h' 16 0 0 0 0
+	    renderToMainWithSurf bgsurf $ drawBasicBG $ 2*(max screenWidthHexes screenHeightHexes)`div`3
+	    return $ Just bgsurf
+	else return Nothing
+    modify $ \ds -> ds { bgSurface = mbg }
+
+    clearMiniLocks
+
+    when (isNothing font) $ lift.putStr $ "Warning: font file not found at "++fontpath++".\n"
+
+drawMsgLine = void.runMaybeT $ do
+    (col,str) <- msum
+	[ ((,) dimWhiteCol) <$> MaybeT (gets hoverStr)
+	, MaybeT $ gets message
+	]
+    lift $ do
+	renderToMain $ blankRow messageLineCentre
+	smallFont <- gets dispFontSmall
+	renderToMain $
+	    (if length str > screenWidthHexes * 3 then withFont smallFont else id) $
+		renderStrColAt col str messageLineCentre
+
+setMsgLineNoRefresh col str = do
+    modify $ \s -> s { message = Just (col,str) }
+    unless (null str) $ modify $ \s -> s { hoverStr = Nothing }
+    drawMsgLine 
+setMsgLine col str = setMsgLineNoRefresh col str >> refresh
+
+drawTitle (Just title) = renderToMain $ renderStrColAt messageCol title titlePos
+drawTitle Nothing = return ()
+    
+say = setMsgLine messageCol
+sayError = setMsgLine errorCol
+
+miniLockPos = (-9)<*>hw <+> hu
+lockLinePos = 2<*>(hu <+> neg hw) <+> miniLockPos
+serverPos = 12<*>hv <+> 6<*>neg hu
+serverWaitPos = serverPos <+> hw <+> neg hu
+nextPagePos = serverPos <+> 4<*>neg hv
+randomNamesPos = 9<*>hv <+> neg hu
+codenamePos = (-4)<*>hw <+> 4<*>hv
+undeclsPos = hv <+> neg hw <+> codenamePos
+accessedOursPos = 2<*>hw <+> codenamePos
+locksPos = hw<+>neg hv
+retiredPos = locksPos <+> 10<*>hu <+> (-4)<*>hv
diff --git a/SDLUIMInstance.hs b/SDLUIMInstance.hs
new file mode 100644
--- /dev/null
+++ b/SDLUIMInstance.hs
@@ -0,0 +1,590 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+{-# LANGUAGE FlexibleInstances #-}
+module SDLUIMInstance () where
+
+import Graphics.UI.SDL hiding (flip)
+import qualified Graphics.UI.SDL as SDL
+import qualified Graphics.UI.SDL.TTF as TTF
+import Control.Concurrent.STM
+import Control.Applicative hiding ((<*>))
+import qualified Data.Map as Map
+import Data.Map (Map)
+import Data.Maybe
+import Control.Concurrent (threadDelay)
+import Control.Monad.State
+import Control.Monad.Trans.Maybe
+import Control.Monad.Trans.Reader
+import Data.Word
+import Data.Array
+import Data.List
+import Data.Function (on)
+import Data.Foldable (for_)
+--import Debug.Trace (traceShow)
+
+import MainState
+import Hex
+import Command
+import GameStateTypes
+import Lock
+import KeyBindings
+import Mundanities
+import Metagame
+import Protocol
+import Cache
+import Database
+import ServerAddr
+import Util
+import InputMode
+import SDLRender
+import SDLUI
+
+instance UIMonad (StateT UIState IO) where
+    runUI m = evalStateT m nullUIState
+    drawMainState = do
+	lift $ clearButtons >> clearSelectables
+	s <- get
+	let mode = ms2im s
+	lift $ waitFrame
+	drawMainState' s
+	lift . drawTitle =<< getTitle
+	lift $ do
+	    drawButtons mode
+	    drawUIOptionButtons mode
+	    drawMsgLine
+	    refresh
+	where
+	drawMainState' (PlayState { psCurrentState=st, psLastAlerts=alerts, wrenchSelected=wsel }) = do
+	    canUndo <- null <$> gets psGameStateMoveStack
+	    canRedo <- null <$> gets psUndoneStack
+	    lift $ do
+		let selTools = [ idx |
+			(idx, PlacedPiece pos p) <- enumVec $ placedPieces st
+			, or [wsel && isWrench p, not wsel && isHook p] ]
+		drawMainGameState selTools False alerts st
+		registerUndoButtons canUndo canRedo
+	drawMainState' (ReplayState { rsCurrentState=st } ) = do
+	    canUndo <- null <$> gets rsGameStateMoveStack
+	    canRedo <- null <$> gets rsMoveStack
+	    lift $ do
+		drawMainGameState [] False [] st
+		registerUndoButtons canUndo canRedo
+		renderToMain $ drawCursorAt Nothing
+	drawMainState' (EditState { esGameStateStack=(st:sts), esUndoneStack=undostack,
+		selectedPiece=selPiece, selectedPos=selPos }) = lift $ do
+	    drawMainGameState (maybeToList selPiece) True [] st
+	    renderToMain $ drawCursorAt $ if isNothing selPiece then Just selPos else Nothing
+	    registerUndoButtons (null sts) (null undostack)
+	    when (isJust selPiece) $ mapM_ registerButtonGroup
+		[ singleButton (periphery 2 <+> 3<*>hw<+>hu) CmdDelete 0
+		-- , singleButton (periphery 2 <+> 3<*>hw) CmdMerge 4
+		]
+	    drawPaintButtons
+	drawMainState' (MetaState saddr undecls cOnly auth names _ _ rnamestvar _ _ mretired path mlock offset) = do
+	    let ourName = authUser <$> auth
+	    let selName = listToMaybe names
+	    let home = isJust ourName && ourName == selName
+	    lift $ renderToMain $ (erase >> drawCursorAt Nothing)
+	    lift $ maybe (drawEmptyMiniLock miniLockPos)
+		(\lock -> do
+		    drawMiniLock lock miniLockPos
+		    registerSelectable miniLockPos 3 SelOurLock
+		    registerButtonGroup $ singleButton (miniLockPos <+> 2<*>neg hv <+> hu) CmdPrevLock 4
+		    registerButtonGroup $ singleButton (miniLockPos <+> 2<*>neg hv <+> hu <+> neg hw) CmdNextLock 4
+		    )
+		(fst<$>mlock)
+	    lift $ do
+		smallFont <- gets dispFontSmall
+		renderToMain $ withFont smallFont $ renderStrColAtLeft messageCol
+			(saddrStr saddr ++ if cOnly then " (cache only)" else "")
+		    $ serverPos <+> hu
+		renderToMain $ renderStrColAtLeft messageCol path $ lockLinePos <+> hu
+		when (offset>0) $
+		    registerButtonGroup $ singleButton (nextPagePos<+>neg hu) CmdPrevPage 4
+
+	    when (length names > 1) $ lift $ registerButtonGroup $ singleButton
+		(codenamePos <+> 2<*>neg hu <+> hw) CmdBackCodename 0
+
+	    runMaybeT $ do
+		name <- MaybeT (return selName)
+		FetchedRecord fresh err muirc <- lift $ getUInfoFetched 300 name
+		lift $ do
+		    lift $ do
+			unless fresh $ do
+			    smallFont <- gets dispFontSmall
+			    renderToMain $ withFont smallFont $ renderStrColAtLeft (opaquify $ dim errorCol) "(stale)" $ serverWaitPos
+			maybe (return ()) (setMsgLineNoRefresh errorCol) err
+			when (fresh && (isNothing ourName || home)) $
+			    registerButtonGroup $ singleButton (codenamePos <+> 2<*>hu)
+				(if isNothing muirc || home then CmdRegister else CmdAuth) 2
+		    (if isJust muirc then drawName else drawNullName) name codenamePos
+		    lift $ registerSelectable codenamePos 0 SelSelectedCodeName
+		    drawRelScore name (codenamePos<+>hu)
+		    for_ muirc $ \(RCUserInfo (_,uinfo)) -> case mretired of 
+			    Just retired -> do
+				fillArea locksPos
+				    (map (locksPos<+>) $ zero:[rotate n $ 4<*>hu<->4<*>hw | n <- [0,2,3,5]])
+				    [ \pos -> (lift $ registerSelectable pos 1 (SelOldLock ls)) >> drawOldLock ls pos
+				    | ls <- retired ]
+				lift $ registerButtonGroup $ singleButton retiredPos CmdShowRetired pubWheelAngle
+				lift $ registerButtonGroup $ singleButton (retiredPos <+> hw) (CmdPlayLockSpec Nothing) 4
+			    Nothing -> do
+				sequence_ [ drawLockInfo (ActiveLock (codename uinfo) i) mlockinfo |
+				    (i,mlockinfo) <- assocs $ userLocks uinfo ]
+				let mmiscpos = (serverPos <+> 2<*>neg hv)
+				when (isJust $ msum $ elems $ userLocks uinfo) $ lift $ do
+				    registerButtonGroup
+					([ Button mmiscpos (CmdSolve Nothing), Button (mmiscpos<+>hu) (CmdViewSolution Nothing)], (2,2))
+				let tested = maybe False (isJust.snd) mlock
+				when (isJust mlock && home) $ lift $ registerButtonGroup $
+					singleButton (miniLockPos <+> 2<*>hv <+> neg hu) (CmdPlaceLock Nothing) $ if tested then 2 else 0
+				lift $ registerButtonGroup $ singleButton retiredPos CmdShowRetired pubWheelAngle
+
+	    when home $ do
+		unless (null undecls) $ do
+		    lift.renderToMain $ renderStrColAtLeft messageCol "Undeclared:" (undeclsPos<+>2<*>hv)
+		    lift $ registerButtonGroup $ singleButton (undeclsPos<+>2<*>hv<+>neg hu) (CmdDeclare Nothing) 2
+		    fillArea (undeclsPos<+>hv)
+			(map (undeclsPos<+>) $ hexDisc 1 ++ [hu<+>neg hw, neg hu<+>hv])
+			[ \pos -> (lift $ registerSelectable pos 0 (SelUndeclared undecl)) >> drawActiveLock al pos
+			| undecl@(Undeclared _ _ al) <- undecls ]
+		rnames <- liftIO $ atomically $ readTVar rnamestvar
+		unless (null rnames) $
+		    fillArea randomNamesPos
+			(map (randomNamesPos<+>) $ hexDisc 2)
+			[ \pos -> (lift $ registerSelectable pos 0 (SelRandom name)) >> drawName name pos
+			| name <- rnames ]
+
+	    when (ourName /= selName) $ void $ runMaybeT $ do
+		when (isJust ourName) $
+		    lift.lift $ registerButtonGroup $ singleButton (codenamePos <+> neg hu <+> hw) CmdHome 4
+		sel <- MaybeT $ return selName
+		us <- MaybeT $ return ourName
+		ourUInfo <- mgetUInfo us
+		selUInfo <- mgetUInfo sel
+		let accesses = map (uncurry getAccessInfo) [(ourUInfo,sel),(selUInfo,us)]
+		lift $ do 
+		    fillArea (codenamePos<+>3<*>hv<+>hw) (map ((codenamePos<+>3<*>hv)<+>) [zero,hw,neg hv])
+			[ \pos -> (lift $ registerSelectable pos 0 SelOurAL) >>
+			    drawNameWithCharAndCol us white (lockIndexChar i) col pos
+			| i <- [0..2]
+			, let (pub,access) = accesses !! 0 !! i
+			, let col
+				| pub = obscure pubColour 
+				| access = obscure $ scoreColour $ -3 
+				| otherwise = dim $ scoreColour 3 ]
+		    fillArea (codenamePos<+>2<*>neg hw) (map ((codenamePos<+>3<*>neg hw)<+>) [zero,hw,neg hv])
+			[ \pos -> (lift $ registerSelectable pos 0 (SelLock $ ActiveLock sel i)) >>
+			    drawNameWithCharAndCol sel white (lockIndexChar i) col pos
+			| i <- [0..2]
+			, let (pub,access) = accesses !! 1 !! i
+			, let col
+				| pub = obscure pubColour 
+				| access = obscure $ scoreColour $ 3 
+				| otherwise = dim $ scoreColour $ -3 ]
+	drawMainState' _ = return ()
+
+    drawMessage = say
+    drawError = sayError
+
+    drawAlerts alerts = return ()
+
+    showHelp _ = return ()
+
+    getChRaw = do
+	events <- liftIO getEvents
+	case listToMaybe $ [ key | KeyDown key <- events ] of
+	    Nothing -> getChRaw
+	    Just (Keysym _ _ ch) -> return $ Just ch
+
+    setUIBinding mode cmd ch =
+	modify $ \s -> s { uiKeyBindings =
+		Map.insertWith (\[bdg] -> \bdgs -> if bdg `elem` bdgs then delete bdg bdgs else bdgs ++ [bdg])
+		    mode [(ch,cmd)] $ uiKeyBindings s }
+
+    initUI = liftM isJust (runMaybeT $ do
+	catchIOErrorMT $ SDL.init [InitVideo]
+	catchIOErrorMT TTF.init
+	lift $ do
+	    readUIConfigFile
+	    initVideo 0 0
+	    liftIO $ setCaption "intricacy" "intricacy"
+	    w <- gets scrWidth
+	    h <- gets scrHeight
+	    liftIO $ warpMouse (fromIntegral $ w`div`2) (fromIntegral $ h`div`2)
+	    renderToMain $ erase
+	    liftIO $ enableUnicode True
+	    liftIO $ enableKeyRepeat 250 30
+	    readBindings
+	)
+	where
+	    catchIOErrorMT m = MaybeT $ liftIO $ catchIO (m >> return (Just ())) (\_ -> return Nothing)
+
+    endUI = do
+	writeUIConfigFile
+	writeBindings
+	liftIO $ quit
+
+    unblockInput = return $ pushEvent VideoExpose
+    suspend = return ()
+    redraw = return ()
+
+    getDrawImpatience = do
+	curState <- get
+	let pos = serverWaitPos
+	return $ \ticks -> void $ flip runStateT curState $ do 
+	    when (ticks>2) $ renderToMain $ do
+		mapM (drawAtRel (filledHexGlyph $ bright black)) [ pos <+> i<*>hu | i <- [0..3] ]
+		withFont (dispFontSmall curState) $
+		    renderStrColAtLeft errorCol ("waiting..."++replicate (ticks`mod`3) '.') $ pos
+	    refresh
+
+    warpPointer pos = do
+	(scrCentre, size) <- getGeom
+	centre <- gets dispCentre
+	let SVec x y = hexVec2SVec size (pos<->centre) <+> scrCentre
+	liftIO $ warpMouse (fromIntegral x) (fromIntegral y)
+	lbp <- gets leftButtonDown
+	rbp <- gets rightButtonDown
+	let [lbp',rbp'] = fmap (fmap (\_ -> (pos<->centre))) [lbp,rbp]
+	modify $ \s -> s {leftButtonDown = lbp', rightButtonDown = rbp'}
+
+    getInput mode = do
+	events <- liftIO $ getEvents
+	oldUIState <- get
+	cmds <- concat <$> mapM processEvent events
+	newUIState <- get
+	return $ cmds ++ if uistatesMayVisiblyDiffer oldUIState newUIState then [CmdRefresh] else []
+	where
+	    uistatesMayVisiblyDiffer uis1 uis2 =
+		uis1 { mousePos = (zero,False), lastFrameTicks=0 }
+		/= uis2 {mousePos = (zero,False), lastFrameTicks=0 }
+	    processEvent (KeyDown (Keysym _ _ ch)) = case mode of
+		IMTextInput -> return [CmdInputChar ch]
+		_ -> do
+		    setting <- gets settingBinding
+		    if isJust setting && ch /= '\0'
+		    then do
+			modify $ \s -> s {settingBinding = Nothing}
+			when (ch /= '\ESC') $ setUIBinding mode (fromJust setting) ch
+			return []
+		    else do
+			uibdgs <- Map.findWithDefault [] mode `liftM` gets uiKeyBindings
+			let mCmd = lookup ch $ uibdgs ++ bindings mode
+			sequence_
+			    [ modify $ \s -> s { paintTileIndex = pti }
+				| (pti,pt) <- zip [0..] paintTiles
+				, (isNothing pt && mCmd == Just CmdWait) ||
+				    (isJust $ do
+					pt' <- pt
+					CmdTile t <- mCmd
+					guard $ ((==)`on`tileType) t pt') ]
+			return $ maybeToList mCmd
+	    processEvent (MouseMotion {}) = do
+		(oldMPos,_) <- gets mousePos
+		(pos@(mPos,_),(sx,sy,sz)) <- getMousePos
+		updateMousePos mode pos
+		lbp <- gets leftButtonDown
+		rbp <- gets rightButtonDown
+		centre <- gets dispCentre
+		let drag :: Maybe HexVec -> Maybe Command
+		    drag bp = do
+			fromPos@(HexVec x y z) <- bp
+			-- check we've dragged at least a full hex's distance:
+			guard $ not.all (\(a,b) -> abs ((fromIntegral a) - b) < 1.0) $ [(x,sx),(y,sy),(z,sz)]
+			let dir = hexVec2HexDirOrZero $ mPos <-> fromPos
+			guard $ dir /= zero
+			return $ CmdDrag (fromPos<+>centre) dir
+		case mode of
+		    IMEdit -> case drag rbp of
+			Just cmd -> return [cmd]
+			Nothing -> if mPos /= oldMPos
+			    then do
+				pti <- getEffPaintTileIndex
+				return $ [ CmdMoveTo $ mPos <+> centre ] ++
+				    (if isJust lbp then [ CmdPaintFromTo (paintTiles!!pti) (oldMPos<+>centre) (mPos<+>centre) ] else [])
+			    else return []
+		    IMPlay -> return $ maybeToList $ msum $ map drag [lbp, rbp]
+		    _ -> return []
+		where
+		    mouseFromTo from to = do
+			let dir = hexVec2HexDirOrZero $ to <-> from
+			if dir /= zero
+			    then (CmdDir WHSSelected dir:) <$> mouseFromTo (from <+> dir) to
+			    else return []
+	    processEvent (MouseButtonDown _ _ ButtonLeft) = do
+		pos@(mPos,central) <- gets mousePos
+		modify $ \s -> s { leftButtonDown = Just mPos }
+		rb <- isJust <$> gets rightButtonDown
+		mcmd <- cmdAtMousePos pos mode (Just False)
+		let hotspotAction = listToMaybe
+			$ map (\cmd -> return [cmd]) (maybeToList mcmd)
+			++ [ (modify $ \s -> s {paintTileIndex = i}) >> return []
+			    | i <- take (length paintTiles) [0..]
+			    , mPos == paintButtonStart <+> i<*>hv ]
+			++ [ toggleUIOption uiOB1 >> updateHoverStr >> return []
+			    | mPos == uiOptPos uiOB1 ]
+			++ [ toggleUIOption uiOB2 >> updateHoverStr >> return []
+			    | mPos == uiOptPos uiOB2 ]
+			++ [ toggleUIOption uiOB3 >> updateHoverStr >> return []
+			    | mPos == uiOptPos uiOB3 ]
+
+		if rb
+		then return [ CmdWait ]
+		else flip fromMaybe hotspotAction $ case mode of
+		    IMEdit -> do
+			pti <- getEffPaintTileIndex
+			return $ [ drawCmd (paintTiles!!pti) False ]
+		    IMPlay -> do
+			centre <- gets dispCentre
+			return $ [ CmdManipulateToolAt $ mPos <+> centre ]
+		    _ -> return []
+	    processEvent (MouseButtonUp _ _ ButtonLeft) = do
+		modify $ \s -> s { leftButtonDown = Nothing }
+		return []
+	    processEvent (MouseButtonDown _ _ ButtonRight) = do
+		pos@(mPos,_) <- gets mousePos
+		modify $ \s -> s { rightButtonDown = Just mPos }
+		(fromMaybe [] <$>) $ runMaybeT $ msum
+		    [ do
+			cmd <- MaybeT $ cmdAtMousePos pos mode Nothing
+			modify $ \s -> s { settingBinding = Just cmd }
+			return []
+		    , do
+			cmd <- MaybeT $ cmdAtMousePos pos mode (Just True)
+			return [cmd]
+		    , case mode of
+			IMPlay -> do
+			    centre <- gets dispCentre
+			    return $ [ CmdManipulateToolAt $ mPos <+> centre ]
+			_ -> return [ CmdSelect ] ]
+	    processEvent (MouseButtonUp _ _ ButtonRight) = do
+		modify $ \s -> s { rightButtonDown = Nothing }
+		return [ CmdUnselect ]
+	    processEvent (MouseButtonDown _ _ ButtonWheelUp) = doWheel 1
+	    processEvent (MouseButtonDown _ _ ButtonWheelDown) = doWheel $ -1
+	    processEvent (MouseButtonDown _ _ ButtonMiddle) = do
+		(mPos,_) <- gets mousePos
+		modify $ \s -> s { middleButtonDown = Just mPos }
+		rb <- isJust <$> gets rightButtonDown
+		return $ if rb then [ CmdDelete ] else []
+	    processEvent (MouseButtonUp _ _ ButtonMiddle) = do
+		modify $ \s -> s { middleButtonDown = Nothing }
+		return []
+	    processEvent (VideoResize w h) = do
+		initVideo w h
+		return [ CmdRedraw ]
+	    processEvent VideoExpose = return [ CmdRedraw ]
+	    processEvent Quit = return [ CmdForceQuit ]
+		
+	    processEvent _ = return []
+
+	    doWheel dw = do
+		rb <- isJust <$> gets rightButtonDown
+		mb <- isJust <$> gets middleButtonDown
+		if rb || mode == IMPlay && not mb
+		    then return [ CmdRotate dw ]
+		    else if mb 
+			then return [ if dw == 1 then CmdUndo else CmdRedo ]
+			else do
+			modify $ \s -> s { paintTileIndex = (paintTileIndex s + dw) `mod` (length paintTiles) }
+			return []
+		
+
+	    drawCmd mt True = CmdPaint mt
+	    drawCmd (Just t) False = CmdTile t
+	    drawCmd Nothing _ = CmdWait
+
+	    getMousePos :: UIM ((HexVec,Bool),(Double,Double,Double))
+	    getMousePos = do
+		(scrCentre, size) <- getGeom
+		(x,y,_) <- lift getMouseState
+		let sv = (SVec (fromIntegral x) (fromIntegral y)) <+> neg scrCentre
+		let mPos@(HexVec x y z) = sVec2HexVec size sv
+		let (sx,sy,sz) = sVec2dHV size sv
+		let isCentral = all (\(a,b) -> abs ((fromIntegral a) - b) < 0.5) $
+			[(x,sx),(y,sy),(z,sz)]
+		return ((mPos,isCentral),(sx,sy,sz))
+	    updateMousePos mode newPos = do
+		oldPos <- gets mousePos
+		when (newPos /= oldPos) $ do
+		    modify $ \ds -> ds { mousePos = newPos }
+		    updateHoverStr 
+
+	    updateHoverStr = do
+		p@(mPos,isCentral) <- gets mousePos
+		hstr <- runMaybeT $ msum
+		    [ MaybeT ( cmdAtMousePos p mode Nothing ) >>= lift . describeCommandAndKeys
+		    , guard (mPos == uiOptPos uiOB1) >> describeUIOptionButton uiOB1
+		    , guard (mPos == uiOptPos uiOB2) >> describeUIOptionButton uiOB2
+		    , guard (mPos == uiOptPos uiOB3) >> describeUIOptionButton uiOB3 ]
+		modify $ \ds -> ds { hoverStr = hstr }
+	    describeCommandAndKeys :: Command -> UIM String
+	    describeCommandAndKeys cmd = do
+		uibdgs <- Map.findWithDefault [] mode `liftM` gets uiKeyBindings
+		return $ describeCommand cmd ++ " ["
+		    ++ concat (intersperse ","
+			(map showKey $ findBindings (uibdgs ++ bindings mode) cmd))
+		    ++ "]"
+
+getEvents = do
+    e <- waitEvent
+    es <- pollEvents
+    return $ e:es
+
+pollEvents = do
+    e <- pollEvent
+    case e of
+	NoEvent -> return []
+	_ -> do
+	    es <- pollEvents
+	    return $ e:es
+
+
+fillArea :: HexVec -> [HexVec] -> [HexVec -> StateT MainState UIM ()] -> StateT MainState UIM () 
+fillArea centre area draws = do
+    selDraws <- do
+	    offset <- gets listOffset
+	    let na = length area
+		nd = length draws
+	    when (nd > (na*(offset+1))) $ lift $
+		registerButtonGroup $ singleButton nextPagePos CmdNextPage 4
+	    return $ drop (max 0 $ min (nd - na) (na*offset)) $ draws
+    sequence_ $ map (uncurry ($)) $
+	zip selDraws $ sortBy
+		(compare `on` (hexLen . (<->centre)))
+		area
+
+drawOldLock ls pos = void.runMaybeT $ msum [ do
+	lock <- mgetLock ls
+	lift.lift $ drawMiniLock lock pos
+    , lift.lift.renderToMain $
+	    renderStrColAt messageCol (show ls) pos
+    ]
+    
+
+drawName name pos = nameCol name >>= drawNameCol name pos
+drawNullName name pos = drawNameCol name pos $ invisible white
+drawNameCol name pos col = do
+    lift.renderToMain $ do 
+	drawAtRel (playerGlyph col) pos
+	renderStrColAt messageCol name pos
+drawRelScore name pos = do
+    col <- nameCol name
+    relScore <- getRelScore name
+    flip (maybe (return ())) relScore $ \score ->
+	lift.renderToMain $ renderStrColAt col
+	    ((if score > 0 then "+" else "") ++ show score) pos
+
+drawNote note pos = case noteBehind note of
+    Just al -> drawActiveLock al pos
+    Nothing -> drawPublicNote (noteAuthor note) pos
+drawActiveLock (ActiveLock name i) =
+    drawNameWithChar name white (lockIndexChar i)
+drawPublicNote name =
+    drawNameWithChar name pubColour 'P'
+drawNameWithChar name charcol char pos = do
+    col <- nameCol name
+    drawNameWithCharAndCol name charcol char col pos
+drawNameWithCharAndCol name charcol char col pos = do
+    size <- fromIntegral.snd <$> lift getGeom
+    let up = SVec 0 $ - (ysize size - size`div`2)
+    let down = SVec 0 $ ysize size
+    smallFont <- lift $ gets dispFontSmall
+    lift.renderToMain $ do
+	drawAtRel (playerGlyph col) pos
+	displaceRender up $ 
+	    renderStrColAt messageCol name pos
+	displaceRender down $ withFont smallFont $
+	    renderStrColAt charcol [char] pos
+pubWheelAngle = 5
+pubColour = colourWheel pubWheelAngle -- ==purple
+nameCol name = do
+    ourName <- (authUser <$>) <$> gets curAuth
+    relScore <- getRelScore name
+    return $ dim $ case relScore of
+	    Nothing -> Pixel $ if ourName == Just name then 0xc0c0c000 else 0x80808000
+	    Just score -> scoreColour score
+scoreColour :: Int -> Pixel
+scoreColour score = Pixel $ case score of
+	0 -> 0x80800000
+	1 -> 0x70a00000 
+	2 -> 0x40c00000
+	3 -> 0x00ff0000
+	(-1) -> 0xa0700000 
+	(-2) -> 0xc0400000
+	(-3) -> 0xff000000
+
+drawLockInfo :: ActiveLock -> Maybe LockInfo -> StateT MainState UIM ()
+drawLockInfo al@(ActiveLock name i) Nothing = do
+    let centre = hw<+>neg hv <+> 7*(i-1)<*>hu
+    lift $ drawEmptyMiniLock centre
+    drawNameWithCharAndCol name white (lockIndexChar i) (invisible white) centre
+    lift $ registerSelectable centre 3 $ SelLockUnset al
+drawLockInfo al@(ActiveLock name i) (Just lockinfo) = do
+    let centre = locksPos <+> 7*(i-1)<*>hu
+    let accessedByPos = centre <+> 3<*>(hv <+> neg hw)
+    let accessedPos = centre <+> 2<*>(hw <+> neg hv)
+    let notesPos = centre <+> 3<*>(hw <+> neg hv)
+    ourName <- (authUser <$>) <$> gets curAuth
+    runMaybeT $ msum [
+	do
+	    lock <- mgetLock $ lockSpec lockinfo
+	    lift.lift $ do
+		drawMiniLock lock centre
+		registerSelectable centre 3 $ SelLock al
+	, lift $ do
+		drawActiveLock al centre
+		lift $ registerSelectable centre 3 $ SelLock al
+	]
+
+    lift.renderToMain $ renderStrColAt dimWhiteCol "Accessed by:" $ accessedByPos <+> hv
+    if public lockinfo
+    then lift.renderToMain $ renderStrColAt pubColour "Everyone!" accessedByPos
+    else if null $ accessedBy lockinfo
+	then lift.renderToMain $ renderStrColAt messageCol "No-one" accessedByPos
+	else fillArea accessedByPos
+		[ accessedByPos <+> d | j <- [0..2], i <- [-2..3]
+		    , i-j > -4, i-j < 3
+		    , let d = j<*>hw <+> i<*>hu ]
+		$ [ \pos -> (lift $ registerSelectable pos 0 (SelSolution note)) >> drawNote note pos
+		    | note <- lockSolutions lockinfo ] ++
+		[ \pos -> (lift $ registerSelectable pos 0 (SelAccessed name)) >> drawName name pos
+		    | name <- accessedBy lockinfo \\ map noteAuthor (lockSolutions lockinfo) ]
+    
+    undecls <- gets undeclareds
+    if isJust $ guard . (|| public lockinfo) . (`elem` map noteAuthor (lockSolutions lockinfo)) =<< ourName
+    then lift.renderToMain $ 
+	(if public lockinfo
+	    then renderStrColAt pubColour "Accessed!"
+	    else renderStrColAt green "Solved!")
+	accessedPos
+    else if any (\(Undeclared _ ls _) -> ls == lockSpec lockinfo) undecls
+    then lift.renderToMain $ renderStrColAt yellow "Undeclared" accessedPos
+    else do
+	read <- take 3 <$> getNotesReadOn lockinfo
+	unless (ourName == Just name) $ do
+	    lift.renderToMain $ renderStrColAt (if length read == 3 then accessedCol else dimWhiteCol)
+		"Read:" $ accessedPos <+> (-3)<*>hu
+	    fillArea (accessedPos<+>neg hu) [ accessedPos <+> i<*>hu | i <- [-1..1] ]
+		$ take 3 $ [ \pos -> (lift $ registerSelectable pos 0 (SelReadNote note)) >> drawName (noteAuthor note) pos
+		    | note <- read ] ++ (repeat $ lift . renderToMain . drawAtRel (hollowGlyph $ dim green))
+
+    lift.renderToMain $ renderStrColAt dimWhiteCol "Holds notes:" $ notesPos <+> hv
+    if null $ notesSecured lockinfo
+	then lift.renderToMain $ renderStrColAt messageCol "None" notesPos
+	else fillArea notesPos
+		[ notesPos <+> d | j <- [0..2], i <- [-2..3]
+		    , i-j > -4, i-j < 3
+		    , let d = j<*>hw <+> i<*>hu ]
+		[ \pos -> (lift $ registerSelectable pos 0 (SelSecured note)) >> drawActiveLock (noteOn note) pos
+		    | note <- notesSecured lockinfo ]
+
diff --git a/Server.hs b/Server.hs
new file mode 100644
--- /dev/null
+++ b/Server.hs
@@ -0,0 +1,394 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module Main where
+
+import Network.Fancy
+
+import Control.Concurrent (threadDelay)
+import Control.Applicative
+import Control.Monad
+import Control.Monad.Trans.State
+import Control.Monad.Trans.Error
+import Control.Monad.Trans.Maybe
+import Control.Monad.Trans.Reader
+import Control.Monad.Trans
+import Control.Monad.IO.Class
+import System.Random
+import Data.Maybe
+import Data.Word
+import Data.Foldable (for_)
+import Data.Time.Clock
+import System.IO
+import System.FilePath
+import qualified Data.ByteString.Lazy as L
+import qualified Data.Binary as B
+import Data.Array
+import Control.Exception
+import System.IO.Error
+import Data.List
+import Data.Function (on)
+import Pipes
+import qualified Pipes.Prelude as P
+
+import System.Environment
+import System.Console.GetOpt
+
+import Protocol
+import Metagame
+import Lock
+import Frame
+import Database
+import AsciiLock
+import Mundanities
+
+port = 27001 -- 27001 == ('i'<<8) + 'y'
+
+data Opt = RequestDelay Int
+    deriving (Eq, Ord, Show)
+options =
+    [ Option ['d'] ["delay"] (ReqArg (RequestDelay . read) "MICROSECS") "delay before sending response (for testing) (default: 0)"
+    --, Option ['d'] ["dir"] (ReqArg (DBDir . read)) "directory for server database [default: intricacydb]"
+    ]
+parseArgs :: [String] -> IO ([Opt],[String])
+parseArgs argv =
+    case getOpt Permute options argv of
+	(o,n,[]) -> return (o,n)
+	(_,_,errs) -> ioError (userError (concat errs ++ usageInfo header options))
+    where header = "Usage: intricacy-server [OPTION...]"
+
+main = do 
+    argv <- getArgs
+    (opts,_) <- parseArgs argv
+    let delay = fromMaybe 0 $ listToMaybe [ d | RequestDelay d <- opts ]
+    withDB setDefaultServerInfo
+    writeFile lockFilePath ""
+    streamServer serverSpec{address = IPv4 "" port, threading=Inline} $ handler delay
+    sleepForever
+
+-- database path is relative to current directory: ./intricacydb/
+dbpath = "intricacydb"
+withDB = flip runReaderT dbpath
+
+setDefaultServerInfo = do
+    alreadySet <- recordExists RecServerInfo
+    unless alreadySet $ putRecord RecServerInfo (RCServerInfo defaultServerInfo)
+
+logit = putStrLn
+
+handler :: Int -> Handle -> Address -> IO ()
+handler delay hdl addr = handle ((\e -> return ()) :: SomeException -> IO ()) $
+    handler' hdl addr
+    where handler' hdl addr = do
+	    response <- handle (\e -> return $ ServerError $ show (e::SomeException)) $ do
+		request <- B.decode <$> L.hGetContents hdl 
+		let hostname = case addr of
+			IP n _ -> n
+			IPv4 n _ -> n
+			IPv6 n _ -> n
+			Unix path -> path
+		    hashedHostname = take 8 $ hash hostname
+		now <- liftIO getCurrentTime
+		logit $ show now ++ ": " ++ hashedHostname ++ " >>> " ++ showRequest request
+		response <- handleRequest request
+		when (delay > 0) $ threadDelay delay
+		now' <- liftIO getCurrentTime
+		logit $ show now' ++ ": " ++ hashedHostname ++ " <<< " ++ showResponse response
+		return response
+	    L.hPut hdl $ B.encode response
+
+showRequest :: ClientRequest -> String
+showRequest (ClientRequest ver mauth act) = show ver ++ " "
+    ++ maybe "" (\(Auth name pw) -> "Auth:" ++ name ++ ":" ++ take 4 pw) mauth ++ " "
+    ++ showAction act
+showAction :: Action -> String
+showAction (SetLock lock idx soln) = "SetLock " ++ show idx ++ " lock:"
+    ++ (if not $ validLock $ reframe lock then " [INVALID LOCK] " else "\n" ++ unlines (lockToAscii lock))
+    ++ "[SOLN]"
+showAction (DeclareSolution soln ls target idx) = "DeclareSolution [SOLN] "
+    ++ (concat $ intersperse " " $ [show ls,show target,show idx])
+showAction act = show act
+showResponse :: ServerResponse -> String
+showResponse (ServedLock lock) = "ServedLock lock:\n" ++ unlines (lockToAscii lock)
+showResponse (ServedSolution soln) = "ServedSolution [SOLN]"
+showResponse resp = show resp
+
+-- | We lock the whole database during each request, using haskell's native
+-- file locking, meaning that we have at any time one writer *xor* any number
+-- of readers.
+withDBLock lockMode m = do
+    h <- getDBLock lockMode
+    ret <- m
+    hClose h
+    return ret
+    where getDBLock lockMode =
+	    catchIO (openFile lockFilePath lockMode) (\_ -> threadDelay (50*10^3) >> getDBLock lockMode)
+lockFilePath = dbpath ++ [pathSeparator] ++ "lockfile"
+
+handleRequest :: ClientRequest -> IO ServerResponse
+handleRequest req@(ClientRequest pv auth action) = do
+    let lockMode = case action of
+	    Authenticate -> ReadMode
+	    GetServerInfo -> ReadMode
+	    GetLock _ -> ReadMode
+	    GetUserInfo _ _ -> ReadMode
+	    GetRetired _ -> ReadMode
+	    GetSolution _ -> ReadMode
+	    GetRandomNames _ -> ReadMode
+	    _ -> ReadWriteMode
+    eresp <- withDBLock lockMode $ runErrorT handleRequest'
+    case eresp of
+	Left error -> return $ ServerError error
+	Right resp -> return resp
+    where
+	handleRequest' = do
+	    when (pv /= protocolVersion) $ throwError "Bad protocol version"
+	    case action of
+		Authenticate -> do 
+		    checkAuth auth 
+		    return $ ServerMessage $ "Welcome, " ++ authUser (fromJust auth)
+		Register -> newUser auth >> return ServerAck
+		ResetPassword passwd -> resetPassword auth passwd >> return ServerAck
+		GetServerInfo -> ServedServerInfo <$> getServerInfo
+		GetLock ls -> ServedLock <$> getLock ls
+		GetRetired name -> ServedRetired <$> getRetired name
+		GetUserInfo name mversion -> (do
+			RCUserInfo (curV,info) <- getRecordErrored $ RecUserInfo name
+			(fromJust<$>)$ runMaybeT $ msum [ do
+			    v <- MaybeT $ return mversion
+			    msum [ guard (v >= curV) >> return ServerFresh
+				, do 
+				    guard (v >= curV - 10) 
+				    RCUserInfoDeltas deltas <- lift $ getRecordErrored $ RecUserInfoLog name
+				    return $ ServedUserInfoDeltas $ take (curV-v) deltas
+				]
+			    , return $ ServedUserInfo (curV,info)
+			    ]
+		    ) `catchError` \_ -> return ServerCodenameFree
+		GetSolution note -> do 
+		    uinfo <- getUserInfoOfAuth auth
+		    let uname = codename uinfo
+		    onLinfo <- getALock $ noteOn note
+		    behindMLinfo <- maybe (return Nothing) ((Just<$>).getALock) $ noteBehind note
+		    if uname == lockOwner (noteOn note)
+			|| uname == noteAuthor note
+		    then ServedSolution <$> getSolution note
+		    else if case behindMLinfo of
+				Nothing -> True
+				Just behindInfo -> uname `elem` accessedBy behindInfo
+			    || note `elem` notesRead uinfo
+			then if public onLinfo || uname `elem` accessedBy onLinfo
+			    then ServedSolution <$> getSolution note
+			    else throwError "You can't wholly decipher that note."
+			else throwError "You don't have access to that note."
+		DeclareSolution soln ls target idx -> do
+		    info <- getUserInfoOfAuth auth
+		    lock <- getLock ls
+		    tinfo <- getALock target
+		    when (ls /= lockSpec tinfo) $ throwError "Lock no longer in use!"
+		    when (public tinfo) $ throwError "Lock solution already public knowledge!"
+		    let name = codename info
+		    let behind = ActiveLock name idx
+		    let note = NoteInfo name (Just behind) target
+		    when (name `elem` map noteAuthor (lockSolutions tinfo)) $
+			throwError "Note already taken on that lock!"
+		    when (name == lockOwner target) $
+			throwError "That's your lock!"
+		    behindLock <- getALock behind
+		    when (public behindLock) $ throwError "Your lock is cracked!"
+		    unless (checkSolution lock soln) $ throwError "Bad solution"
+		    erroredDB $ putRecord (RecNote note) (RCSolution soln)
+		    execStateT (declareNote note behind) [] >>= applyDeltasToRecords
+		    return ServerAck
+		SetLock lock@(frame,_) idx soln -> do
+		    info <- getUserInfoOfAuth auth
+		    let name = codename info
+		    let al = ActiveLock name idx
+		    ServerInfo serverSize _ <- getServerInfo
+		    when (frame /= BasicFrame serverSize) $ throwError $
+			"Server only accepts size "++show serverSize++" locks."
+		    unless (validLock $ reframe lock) $ throwError "Invalid lock!"
+		    unless (not.checkSolved $ reframe lock) $ throwError "Lock not locked!"
+		    unless (checkSolution lock soln) $ throwError "Bad solution"
+
+		    RCLockHashes hashes <- getRecordErrored RecLockHashes
+			    `catchError` const (return (RCLockHashes []))
+		    let hashed = hash $ show lock
+		    when (hashed `elem` hashes) $ throwError "Lock has already been used"
+		    ls <- erroredDB $ newLockRecord lock
+		    erroredDB $ putRecord RecLockHashes $ RCLockHashes $ hashed:hashes
+
+		    let oldLockInfo = userLocks info ! idx
+		    execStateT (do
+			    when (isJust $ oldLockInfo) $
+				lift (getALock al) >>= retireLock
+			    addDelta name $ PutLock ls idx
+			) [] >>= applyDeltasToRecords
+
+		    for_ oldLockInfo $ \oldui -> do
+			lss <- getRetired name
+			erroredDB $ putRecord (RecRetiredLocks name) $ RCLockSpecs $ (lockSpec oldui):lss
+		    return ServerAck
+		GetRandomNames n -> do
+		    names <- erroredDB $ listUsers
+		    gen <- erroredIO $ getStdGen
+		    let l = length names
+			namesArray = listArray (0,l-1) names
+			negligible name = do
+			    uinfo <- getUserInfo name
+			    return $ all (maybe True public . (userLocks uinfo !)) [0..2]
+
+		    -- huzzah for pipes!
+		    shuffled <- P.toListM $
+			    mapM_ Pipes.yield (nub $ randomRs (0,l-1) gen)
+			>-> P.take l -- give up once we've permuted all of [0..l-1]
+			>-> P.map (namesArray !)
+			>-> P.filterM ((not <$>) . negligible) -- throw away negligibles
+			>-> P.take n -- try to take as many as we were asked for
+		    liftIO newStdGen
+		    return $ ServedRandomNames shuffled
+		_ -> throwError "BUG: bad request"
+	erroredIO :: IO a -> ErrorT String IO a
+	erroredIO c = do
+	    ret <- liftIO $ catchIO (Right <$> c) (return.Left)
+	    case ret of
+		Left e -> throwError $ "Server IO error: " ++ show e
+		Right x -> return x
+	erroredDB :: DBM a -> ErrorT String IO a
+	erroredDB = erroredIO . withDB
+	getRecordErrored :: Record -> ErrorT String IO RecordContents
+	getRecordErrored rec = do
+	    mrc <- lift $ withDB $ getRecord rec 
+	    case mrc of
+		Just rc -> return rc
+		Nothing -> throwError $ "Bad record on server! Record was: " ++ show rec
+	getLock ls = do 
+	    RCLock lock <- getRecordErrored $ RecLock ls
+	    return lock
+	getSolution note = do 
+	    RCSolution soln <- getRecordErrored $ RecNote note
+	    return soln
+	getServerInfo = do
+	    RCServerInfo sinfo <- getRecordErrored $ RecServerInfo
+	    return sinfo
+	getRetired name = do
+	    RCLockSpecs lss <- fromMaybe (RCLockSpecs []) <$> (erroredDB $ getRecord $ RecRetiredLocks name)
+	    return lss
+	getALock (ActiveLock name idx) = do
+	    info <- getUserInfo name
+	    checkValidLockIndex idx
+	    case ((!idx).userLocks) info of
+		Nothing -> throwError "Lock not set"
+		Just lockinfo -> return lockinfo
+	checkValidLockIndex idx =
+	    unless (0<=idx && idx < maxLocks) $ throwError "Bad lock index"
+	getUserInfo name = do 
+	    RCUserInfo (version,info) <- getRecordErrored $ RecUserInfo name
+	    return info
+	getUserInfoOfAuth auth = do 
+	    checkAuth auth
+	    let Just (Auth name _) = auth
+	    getUserInfo name
+	    
+	checkAuth :: Maybe Auth -> ErrorT String IO ()
+	checkAuth Nothing = throwError "Authentication required"
+	checkAuth (Just (Auth name pw)) = do
+	    exists <- checkCodeName name
+	    unless exists $ throwError "No such user"
+	    RCPassword pw' <- getRecordErrored (RecPassword name)
+	    when (pw /= pw') $ throwError "Wrong password"
+	newUser :: Maybe Auth -> ErrorT String IO ()
+	newUser Nothing = throwError "Require authentication"
+	newUser (Just (Auth name pw)) = do
+	    exists <- checkCodeName name
+	    when exists $ throwError "Codename taken"
+	    erroredDB $ putRecord (RecPassword name) (RCPassword pw)
+	    erroredDB $ putRecord (RecUserInfo name) (RCUserInfo $ (1,initUserInfo name))
+	    erroredDB $ putRecord (RecUserInfoLog name) (RCUserInfoDeltas [])
+	resetPassword Nothing pw = throwError "Authentication required"
+	resetPassword auth@(Just (Auth name _)) newpw = do
+	    checkAuth auth
+	    erroredDB $ putRecord (RecPassword name) (RCPassword newpw)
+	checkCodeName :: Codename -> ErrorT String IO Bool
+	checkCodeName name = do
+	    unless (validCodeName name) $ throwError "Invalid codename"
+	    liftIO $ withDB $ recordExists $ RecPassword name
+	--- | TODO: journalling so we can survive death during database writes?
+	applyDeltasToRecords :: [(Codename, UserInfoDelta)] -> ErrorT String IO ()
+	applyDeltasToRecords nds = sequence_ $ [applyDeltasToRecord name deltas
+		| group <- groupBy ((==) `on` fst) nds 
+		, let name = fst $ head group
+		, let deltas = map snd group ]
+	applyDeltasToRecord name deltas = do
+	    erroredDB $ modifyRecord (RecUserInfoLog name) $
+		\(RCUserInfoDeltas deltas') -> RCUserInfoDeltas $ deltas ++ deltas'
+	    erroredDB $ modifyRecord (RecUserInfo name) $
+		\(RCUserInfo (v,info)) -> RCUserInfo $
+		    (v+length deltas, applyDeltas info deltas)
+	declareNote note@(NoteInfo _ _ target) behind@(ActiveLock name idx) = do
+	    accessLock name target =<< getCurrALock target
+	    addDelta (lockOwner target) $ LockDelta (lockIndex target) $ AddSolution note
+	    addDelta name $ LockDelta idx $ AddSecured note
+	    accessed <- accessedBy <$> getCurrALock behind
+	    mapM_ (addReadNote note) (name:accessed)
+	addReadNote note@(NoteInfo _ _ target) name = do
+	    info <- getCurrUserInfo name
+	    tlock <- getCurrALock target
+	    unless (name `elem` accessedBy tlock || note `elem` notesRead info) $ do
+		addDelta name $ AddRead note
+		checkSuffReadNotes target name
+	accessLock name target@(ActiveLock tname ti) tlock = do
+	    addDelta tname $ LockDelta ti $ AddAccessed name
+	    mapM_ (`addReadNote` name) $ notesSecured tlock
+	publiciseLock al@(ActiveLock name idx) lock = do
+	    addDelta name $ LockDelta idx SetPublic
+	    retireLock lock
+	retireLock lock = do
+	    mapM_ scrapNote $ lockSolutions lock
+	    mapM_ publiciseNote $ notesSecured lock
+	scrapNote note@(NoteInfo _ (Just al@(ActiveLock name idx)) _) = do
+	    addDelta name $ LockDelta idx (DelSecured note)
+	    unreadNote note
+	scrapNote _ = return ()
+	unreadNote note@(NoteInfo name (Just al) _) = do
+	    lock <- getCurrALock al
+	    mapM_ (\name' -> addDelta name' (DelRead note)) $ name:(accessedBy lock)
+	publiciseNote note@(NoteInfo _ _ al@(ActiveLock name idx)) = do
+	    unreadNote note
+	    addDelta name $ LockDelta idx $ SetPubNote note
+	    publified <- checkSuffPubNotes al
+	    unless publified $ do
+	    lock <- getCurrALock al
+	    accessorsOfNotesOnLock <- ((++ map noteAuthor (lockSolutions lock)).concat)
+		<$> (sequence 
+		    [ accessedBy <$> getCurrALock behind | NoteInfo _ (Just behind) _ <- lockSolutions lock ] )
+	    forM_ accessorsOfNotesOnLock $ checkSuffReadNotes al
+	checkSuffReadNotes target name = do
+	    info <- getCurrUserInfo name
+	    tlock <- getCurrALock target
+	    let countRead = fromIntegral $ length $
+		    filter (\n -> isNothing (noteBehind n) || n `elem` notesRead info) $ lockSolutions tlock
+	    when (countRead == notesNeeded && not (public tlock) && name /= lockOwner target) $
+		accessLock name target tlock
+	checkSuffPubNotes al@(ActiveLock name idx) = do
+	    info <- getCurrUserInfo name
+	    let Just lock = userLocks info ! idx
+	    let countPub = fromIntegral $ length $
+		    filter (isNothing.noteBehind) $ lockSolutions lock
+	    if (countPub == notesNeeded)
+		then publiciseLock al lock >> return True
+		else return False
+	-- | XXX we apply deltas right-to-left, so in the order of adding
+	addDelta name delta = modify ((name,delta):)
+	getCurrUserInfo name = do
+	    info <- lift $ getUserInfo name
+	    (applyDeltas info . map snd . filter ((==name).fst)) <$> get
+	getCurrALock al@(ActiveLock name idx) =
+	     (fromJust.(!idx).userLocks) <$> getCurrUserInfo name
diff --git a/ServerAddr.hs b/ServerAddr.hs
new file mode 100644
--- /dev/null
+++ b/ServerAddr.hs
@@ -0,0 +1,32 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module ServerAddr where
+
+import Data.List
+import Data.Maybe
+import Control.Applicative
+
+data ServerAddr = ServerAddr {hostname::String, port::Int}
+    deriving (Eq, Ord, Show, Read)
+nullSaddr (ServerAddr host _) = null host
+
+defaultPort=27001 -- == ('i'<<8) + 'y'
+defaultServerAddr = ServerAddr "mbays.mdns.org" defaultPort
+
+saddrStr (ServerAddr h p) = h ++ if p==defaultPort then "" else ':':show p
+
+strToSaddr str =
+    case elemIndex ':' str of
+	Nothing -> Just $ ServerAddr str defaultPort
+	Just idx -> do
+	    let (addr,portstr) = splitAt idx str
+	    port <- fst <$> listToMaybe (reads (drop 1 portstr))
+	    return $ ServerAddr addr port
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,12 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+import Distribution.Simple
+main = defaultMain
diff --git a/Util.hs b/Util.hs
new file mode 100644
--- /dev/null
+++ b/Util.hs
@@ -0,0 +1,16 @@
+-- This file is part of Intricacy
+-- Copyright (C) 2013 Martin Bays <mbays@sdf.org>
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of version 3 of the GNU General Public License as
+-- published by the Free Software Foundation.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see http://www.gnu.org/licenses/.
+
+module Util where
+import qualified Data.Vector as Vector
+import Data.Vector (Vector)
+
+enumVec :: Vector a -> [(Int,a)]
+enumVec = Vector.toList . Vector.indexed
diff --git a/VeraMono.ttf b/VeraMono.ttf
new file mode 100644
Binary files /dev/null and b/VeraMono.ttf differ
diff --git a/intricacy.cabal b/intricacy.cabal
new file mode 100644
--- /dev/null
+++ b/intricacy.cabal
@@ -0,0 +1,99 @@
+name:                intricacy
+version:             0.3
+synopsis:            A game of competitive puzzle-design
+homepage:            http://mbays.freeshell.org/intricacy
+license:             GPL-3
+license-file:        COPYING
+author:              Martin Bays
+maintainer:          mbays@sdf.org
+-- copyright:           
+category:            Game
+build-type:          Simple
+cabal-version:       >=1.8
+data-files: VeraMono.ttf tutorial/*.lock tutorial/*.text
+extra-doc-files: README NEWS tutorial-extra/*.lock tutorial-extra/README
+
+description:
+    A networked game with client-server architecture. The core game is a
+    lockpicking-themed turn-based puzzle game on a hex grid. Players design
+    puzzles (locks) and solve those designed by others. A metagame encourages
+    the design of maximally difficult puzzles, within tight size constraints.
+    The client supports Curses and SDL, with all graphics in SDL mode drawn by
+    code using SDL-gfx. The network protocol is based on the 'binary' package,
+    and is intended to be reasonably efficient. TVars are used to give
+    transparent local caching and background network operations. Also
+    incorporates an implementation of a graph 5-colouring algorithm (see
+    GraphColouring.hs).
+
+source-repository head
+   type:     git
+   location: http://mbays.freeshell.org/intricacy/.git
+
+Flag SDL 
+    Description: Enable SDL UI
+    Default: True
+Flag Curses 
+    Description: Enable Curses UI
+    Default: True
+Flag Game
+    Description: Build game
+    Default: True
+    Manual: True
+Flag Server
+    Description: Build server
+    Default: False
+    Manual: True
+
+executable intricacy
+  if flag(Game)
+      extensions: DoAndIfThenElse
+      build-depends: base >=4.3, base < 5
+        , mtl >=2.0, transformers >=0.2, stm >= 2.1
+        , directory >= 1.0, filepath >= 1.0, time >= 1.2
+        , bytestring >=0.10
+        , array >=0.3, containers >=0.4, vector >=0.9
+        , binary >=0.5, network-fancy >= 0.1.5
+        , cryptohash >= 0.8
+      if flag(SDL)
+          build-depends: SDL >=0.6.5, SDL-ttf >=0.6, SDL-gfx >=0.6
+          if os(windows)
+              Extra-Lib-Dirs:    winlibs
+              Extra-Libraries:   SDL_ttf SDL SDL_gfx freetype
+              ghc-options: -optl-mwindows
+  else
+      Buildable: False
+  if flag(Curses)
+      build-depends: hscurses >=1.4
+  if flag(SDL)
+      if flag(Curses)
+        main-is: MainBoth.hs
+      else
+        main-is: MainSDL.hs
+  else
+      if flag(Curses)
+        main-is: MainCurses.hs
+
+  other-modules: AsciiLock, BinaryInstances, BoardColouring, Cache, Command,
+      CursesRender, CursesUI, CursesUIMInstance, CVec, Database, Debug,
+      EditGameState, Frame, GameState, GameStateTypes, GraphColouring, Hex, Init,
+      InputMode, Interact, InteractUtil, KeyBindings, Lock, MainState,
+      Maxlocksize, Metagame, Mundanities, Physics, Protocol, SDLRender, SDLUI,
+      SDLUIMInstance, ServerAddr, Util
+
+executable intricacy-server
+  if flag(Server)
+      extensions: DoAndIfThenElse
+      build-depends: base >=4.3, base < 5
+        , mtl >=2.0, transformers >=0.2, stm >= 2.1
+        , directory >= 1.0, filepath >= 1.0, time >= 1.2
+        , bytestring >=0.10
+        , array >=0.3, containers >=0.4, vector >=0.9
+        , binary >=0.5, network-fancy >= 0.1.5
+        , cryptohash >= 0.8
+        , random >= 1.0, pipes >= 4
+  else
+    Buildable: False
+  main-is:  Server.hs
+  other-modules: AsciiLock, BinaryInstances, BoardColouring, CVec, Database,
+      Debug, Frame, GameState, GameStateTypes, GraphColouring, Hex, Lock,
+      Maxlocksize, Metagame, Mundanities, Physics, Protocol, Util
diff --git a/tutorial-extra/5-springChains.lock b/tutorial-extra/5-springChains.lock
new file mode 100644
--- /dev/null
+++ b/tutorial-extra/5-springChains.lock
@@ -0,0 +1,17 @@
+        % % % % % % % % %          
+       %                 % % % %   
+      %                   %     %  
+     %                     %     % 
+    % S S S S " S S S S & & & & & %
+   %   o -   "   # # #       %   % 
+  %   /         #     %       % %  
+ %             #     %       [ %   
+%           #       %       [   %  
+ %           " S S %       [   %   
+  % S S S S " "   %       #   %    
+ % %           % % S S S S " %     
+% * '                       %      
+ % @ %                     %       
+  % % %                   %        
+       %                 %         
+        % % % % % % % % %          
diff --git a/tutorial-extra/6-snookered.lock b/tutorial-extra/6-snookered.lock
new file mode 100644
--- /dev/null
+++ b/tutorial-extra/6-snookered.lock
@@ -0,0 +1,17 @@
+        " " " " " " " " "          
+       " S S S # S S S % " " " "   
+      "       #         % "     "  
+     "       #     &     % "     " 
+    "       # O O   &     % % % % "
+   "       # O O   & "     % "   " 
+  "       #       & " " "     " "  
+ "   & & & & & & & % C C # #   "   
+" & &       & & & . o         % "  
+ " O O O O O & & o - '       % "   
+  " O O O O   &   . o         "    
+ " " O O O   & & O % \       "     
+" * ' O O   & &   %   o     "      
+ " @ " O   &     # Z #     "       
+  " " "         # # Z     "        
+       "   # # # # # Z   "         
+        " " " " " " " " "          
diff --git a/tutorial-extra/7-plunger.lock b/tutorial-extra/7-plunger.lock
new file mode 100644
--- /dev/null
+++ b/tutorial-extra/7-plunger.lock
@@ -0,0 +1,17 @@
+        " " " " " " " " "          
+       "     #       # # " " " "   
+      "   #   #     # #   "     "  
+     "     #   ]   # #     " %   " 
+    "   #   # % % # # & & & %     "
+   "     #   ]   % #   7   % "   " 
+  "   #   # " " # '   7   % % " "  
+ "     #   ]   " o % %   '   % "   
+"       # % % # '   7 . o     % "  
+ "   #   ]   % o   7         1 "   
+  "   # " " # ' # #         1 "    
+ " "   #   " o   7         # "     
+" * '     % '   7           "      
+ " @ "   % o % %           "       
+  " " " %     7           "        
+       "     7           "         
+        " " " " " " " " "          
diff --git a/tutorial-extra/8-cogs.lock b/tutorial-extra/8-cogs.lock
new file mode 100644
--- /dev/null
+++ b/tutorial-extra/8-cogs.lock
@@ -0,0 +1,17 @@
+        " " " " " " " " "          
+       " # # # # [ # # # " " " "   
+      " # # # # [ # S S % "     "  
+     " # # # # [       % % "     " 
+    " # # # # [           % % %   "
+   " #   \   % % % %     %   "   " 
+  "       o - ' % [     . o   " "  
+ "   # # / . o   [ # #     `   "   
+"   # # # # \ ` # # [     . o   "  
+ "   # #   ' o - ' [         ` "   
+  "     . o / . o %       #   "    
+ " "       ` #   ` %     #   "     
+" * '     # # # % % % % #   "      
+ " @ "   # # #   % # # #   "       
+  " " "   # # % % # # #   "        
+       "                 "         
+        " " " " " " " " "          
diff --git a/tutorial-extra/README b/tutorial-extra/README
new file mode 100644
--- /dev/null
+++ b/tutorial-extra/README
@@ -0,0 +1,4 @@
+Some harder tutorial levels.
+
+To play these, either run intricacy on them, or move them into the 'tutorial'
+directory, or see user 'TUT' on the main server.
diff --git a/tutorial/1-winning.lock b/tutorial/1-winning.lock
new file mode 100644
--- /dev/null
+++ b/tutorial/1-winning.lock
@@ -0,0 +1,11 @@
+       " " " " " "       
+      "           " " "  
+     "             "   " 
+    " S S S S S S # # # "
+   "             #   " " 
+  "                   "  
+ " "                 "   
+" * '               "    
+ " @ "             "     
+  " " "           "      
+       " " " " " "       
diff --git a/tutorial/1-winning.text b/tutorial/1-winning.text
new file mode 100644
--- /dev/null
+++ b/tutorial/1-winning.text
@@ -0,0 +1,1 @@
+use tools to pull bolt, then Open the lock
diff --git a/tutorial/2-hook.lock b/tutorial/2-hook.lock
new file mode 100644
--- /dev/null
+++ b/tutorial/2-hook.lock
@@ -0,0 +1,11 @@
+       " " " " " "       
+      " # # # # # " " "  
+     " # # # # # # "   " 
+    " # # S S S S & & & "
+   " # # ] #     & & " " 
+  " # # # ] # # #     "  
+ " "   # # ]         "   
+" * ' &     ] # % % "    
+ " @ " Z     % Z % "     
+  " " " Z       Z "      
+       " " " " " "       
diff --git a/tutorial/2-hook.text b/tutorial/2-hook.text
new file mode 100644
--- /dev/null
+++ b/tutorial/2-hook.text
@@ -0,0 +1,1 @@
+the hook is a versatile tool; move and rotate it to push the springs aside
diff --git a/tutorial/3-wrench.lock b/tutorial/3-wrench.lock
new file mode 100644
--- /dev/null
+++ b/tutorial/3-wrench.lock
@@ -0,0 +1,17 @@
+        " " " " " " " " "          
+       "                 " " " "   
+      "   % S S S S S &   "     "  
+     "     O   %       &   "     " 
+    " % % % %   o -   & & & & & & "
+   "         %   %           "   " 
+  "           %   % %   # # # " "  
+ "             %             # "   
+"               % % %   # # # # "  
+ "             % %             "   
+  "             %         O   "    
+ " "         &   % %   % % % "     
+" * ' & & & & &     \     % "      
+ " @ "         & & & o   % "       
+  " " "         &       % "        
+       "         &     % "         
+        " " " " " " " " "          
diff --git a/tutorial/3-wrench.text b/tutorial/3-wrench.text
new file mode 100644
--- /dev/null
+++ b/tutorial/3-wrench.text
@@ -0,0 +1,1 @@
+you have less fine control over the wrench; it moves until stopped
diff --git a/tutorial/4-plug.lock b/tutorial/4-plug.lock
new file mode 100644
--- /dev/null
+++ b/tutorial/4-plug.lock
@@ -0,0 +1,17 @@
+        & & & & & & & & &          
+       & #             [ & & & &   
+      &   #           [   &     &  
+     &     #         (     & # # & 
+    &       #       (   # # #     &
+   & # # # # #     (   #   7 &   & 
+  &         %     (       7   & &  
+ &           Z   (       % % % &   
+&             Z (       # S S % &  
+ &             "               &   
+  &             ]             &    
+ & &             ] % % % % % &     
+& * '             # %       &      
+ & @ &               %     &       
+  & & &               %   &        
+       &               % &         
+        & & & & & & & & &          
diff --git a/tutorial/4-plug.text b/tutorial/4-plug.text
new file mode 100644
--- /dev/null
+++ b/tutorial/4-plug.text
@@ -0,0 +1,1 @@
+Co-ordinate the tools. Keyboard control advised; rebind with ^B or right-click.
