diff --git a/AsciiLock.hs b/AsciiLock.hs
--- a/AsciiLock.hs
+++ b/AsciiLock.hs
@@ -16,6 +16,7 @@
 import           Control.Applicative
 import           Control.Arrow       ((&&&))
 import           Control.Monad
+import           Data.Char           (toUpper)
 import           Data.Function       (on)
 import           Data.List
 import           Data.Map            (Map)
@@ -33,6 +34,7 @@
 import           Hex
 import           Lock
 import           Mundanities
+import           Physics
 import           Util
 
 
@@ -234,16 +236,57 @@
 minmax :: Ord a => [a] -> (a,a)
 minmax = minimum &&& maximum
 
+solutionToAscii :: Solution -> String
+solutionToAscii = map pmToAscii
+
+dirChar :: HexVec -> Char
+dirChar dir
+    | dir == hu = 'l'
+    | dir == neg hu = 'h'
+    | dir == hv = 'y'
+    | dir == neg hv = 'n'
+    | dir == hw = 'b'
+    | dir == neg hw = 'u'
+    | otherwise = '.'
+pmToAscii :: PlayerMove -> Char
+pmToAscii (HookPush dir)    = dirChar dir
+pmToAscii (WrenchPush dir)  = toUpper $ dirChar dir
+pmToAscii (HookTorque 1)    = '+'
+pmToAscii (HookTorque (-1)) = '-'
+pmToAscii _                 = '.'
+
+solutionOfAscii :: String -> Maybe Solution
+solutionOfAscii = mapM pmOfAscii
+
+pmOfAscii :: Char -> Maybe PlayerMove
+pmOfAscii 'l' = Just $ HookPush hu
+pmOfAscii 'y' = Just $ HookPush hv
+pmOfAscii 'b' = Just $ HookPush hw
+pmOfAscii 'h' = Just . HookPush $ neg hu
+pmOfAscii 'n' = Just . HookPush $ neg hv
+pmOfAscii 'u' = Just . HookPush $ neg hw
+pmOfAscii 'L' = Just $ WrenchPush hu
+pmOfAscii 'Y' = Just $ WrenchPush hv
+pmOfAscii 'B' = Just $ WrenchPush hw
+pmOfAscii 'H' = Just . WrenchPush $ neg hu
+pmOfAscii 'N' = Just . WrenchPush $ neg hv
+pmOfAscii 'U' = Just . WrenchPush $ neg hw
+pmOfAscii '+' = Just $ HookTorque 1
+pmOfAscii '-' = Just . HookTorque $ -1
+pmOfAscii '.' = Just NullPM
+pmOfAscii _   = Nothing
+
 readAsciiLockFile :: FilePath -> IO (Maybe Lock, Maybe Solution)
 readAsciiLockFile path = fromLines <$> readStrings path
     where fromLines lines = 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)
+            return (lockOfAscii locklines,
+                solutionOfAscii solnLine `mplus` 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]
+        Just soln -> ["Solution:", solutionToAscii soln]
diff --git a/Cache.hs b/Cache.hs
--- a/Cache.hs
+++ b/Cache.hs
@@ -8,6 +8,8 @@
 -- 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 LambdaCase #-}
+
 module Cache where
 
 import           Control.Applicative
@@ -20,7 +22,7 @@
 import qualified Data.ByteString            as BS
 import qualified Data.ByteString.Lazy       as BL
 import           Data.Maybe
-import           Network.Fancy
+import           Network.Simple.TCP         (connect, recv, send)
 import           System.Directory
 import           System.FilePath
 import           System.IO
@@ -78,14 +80,18 @@
 makeRequest saddr _ | nullSaddr saddr =
     return $ ServerError "No server set."
 makeRequest saddr@(ServerAddr host port) request =
-    handle (return . ServerError . (show::SomeException -> String)) $
-        withStream (IP host port) makeRequest'
+    handle (return . ServerError . (show::SomeException -> String)) $ do
+        connect host (show 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 <$> BS.hGetContents hdl
+        makeRequest' (sock,_) = do
+            send sock . BL.toStrict $ encode request
+            decode . BL.fromStrict <$> recvAll sock
+        recvAll sock =
+            recv sock 4096 >>= \case
+                Nothing -> return BS.empty
+                Just b  -> BS.append b <$> recvAll sock
+
 
 knownServers :: IO [ServerAddr]
 knownServers = ignoreIOErr $ do
diff --git a/CursesUI.hs b/CursesUI.hs
--- a/CursesUI.hs
+++ b/CursesUI.hs
@@ -314,7 +314,7 @@
     drawMsgLine
     refresh
 
-drawTitle (Just title) = do
+drawTitle (Just (title,n)) = do
     (h,w) <- liftIO Curses.scrSize
     drawStrCentred a0 white (CVec 0 (w`div`2)) title
 drawTitle Nothing = return ()
diff --git a/Init.hs b/Init.hs
--- a/Init.hs
+++ b/Init.hs
@@ -30,10 +30,17 @@
 import           Util
 import           Version
 
-data Opt = LockSize Int | ForceCurses | Help | Version
+data Opt
+    = DataDir FilePath
+    | LockSize Int
+    | ForceCurses
+    | Help
+    | Version
     deriving (Eq, Ord, Show)
+
 options =
-    [ Option ['c'] ["curses"] (NoArg ForceCurses) "force curses UI"
+    [ Option ['d'] ["datadir"] (ReqArg DataDir "PATH") "user data and conf directory (default: ~/.intricacy)"
+    , Option ['c'] ["curses"] (NoArg ForceCurses) "force curses UI"
     , Option ['s'] ["locksize"] (ReqArg (LockSize . read) "SIZE") "locksize"
     , Option ['h'] ["help"] (NoArg Help) "show usage information"
     , Option ['v'] ["version"] (NoArg Version) "show version information"
@@ -49,22 +56,24 @@
         (o,n,[])   -> return (o,n)
         (_,_,errs) -> ioError (userError (concat errs ++ usage))
 
-setup :: IO (Maybe Lock,[Opt],Maybe String)
+setup :: IO (Maybe (Lock, Maybe Solution), [Opt], Maybe String)
 setup = do
     argv <- getArgs
     (opts,args) <- parseArgs argv
     when (Help `elem` opts) $ putStr usage >> exitSuccess
     when (Version `elem` opts) $ putStrLn version >> exitSuccess
     let size = fromMaybe 8 $ listToMaybe [ size | LockSize size <- opts ]
+    mapM_ (setEnv "INTRICACY_PATH") [ dir | DataDir dir <- opts ]
+
     curDir <- getCurrentDirectory
     (fromJust <$>) $ runMaybeT $ msum
         [ do
             path <- liftMaybe ((curDir </>) <$> listToMaybe args)
             msum [ do
-                    lock <- reframe.fst <$> MaybeT (readLock path)
-                    return (Just lock,opts,Just path)
-                , return (Just $ baseLock size, opts, Just path) ]
-        , return (Nothing,opts,Nothing) ]
+                    (lock, msoln) <- MaybeT (readLock path)
+                    return (Just (reframe lock, msoln), opts, Just path)
+                , return (Just (baseLock size, Nothing), opts, Just path) ]
+        , return (Nothing, opts, Nothing) ]
 
 main' :: (UIMonad s, UIMonad c) =>
         Maybe (s MainState -> IO (Maybe MainState)) ->
@@ -72,8 +81,8 @@
 main' msdlUI mcursesUI = do
     (mlock,opts,mpath) <- setup
     initMState <- case mlock of
-            Just lock -> return $ newEditState lock Nothing mpath
-            Nothing   -> initMetaState
+            Just (lock, msoln) -> return $ newEditState lock msoln mpath
+            Nothing            -> initMetaState
     void $ runMaybeT $ msum [ do
             finalState <- msum
                 [ do
diff --git a/Interact.hs b/Interact.hs
--- a/Interact.hs
+++ b/Interact.hs
@@ -146,7 +146,7 @@
     title <- lift getTitle
     (lift . lift . confirm) ("Really quit"
             ++ (if im == IMEdit then " without saving" else "")
-            ++ maybe "" (" from "++) title ++ "?")
+            ++ maybe "" ((" from "++) . fst) title ++ "?")
         >>? throwE $ InteractSuccess False
 processCommand im CmdForceQuit = throwE $ InteractSuccess False
 processCommand IMPlay CmdOpen = do
@@ -196,7 +196,7 @@
         MaybeT . lift $ gets (Map.lookup v . initLocks)
     lift $ do
         (InteractSuccess solved, ps) <- lift . runSubMainState $
-            maybe newPlayState restorePlayState partial (reframe lock) (Just desc) Nothing False True
+            maybe newPlayState restorePlayState partial (reframe lock) [] (Just desc) Nothing False True
         let updateLock initLock = initLock { initLockSolved = initLockSolved initLock || solved
                 , initLockPartial = Just $ savePlayState ps }
         lift . modify $ \is -> is { initLocks = Map.adjust updateLock v $ initLocks is }
@@ -411,7 +411,7 @@
             liftMaybe $ readMay tls
         ]
     RCLock lock <- MaybeT $ getFreshRecBlocking $ RecLock ls
-    solveLock lock $ Just $ "solving " ++ show ls
+    solveLock lock Nothing $ Just $ "solving " ++ show ls
 
 processCommand' IMMeta (CmdDeclare mundecl) = void.runMaybeT $ do
     guard =<< mourNameSelected
@@ -482,7 +482,7 @@
         askLockIndex ("Place " ++ show lockpath ++ " in which slot?") "bug" $ const True
     when (isJust $ userLocks ourUInfo ! idx) $
         confirmOrBail "Really retire existing lock?"
-    soln <- (liftMaybe msoln `mplus`) $ solveLock lock $ Just "testing lock"
+    soln <- (liftMaybe msoln `mplus`) $ solveLock lock Nothing $ Just "testing lock"
     lift $ curServerActionAsyncThenInvalidate
         (SetLock lock idx soln)
         (Just (SomeCodenames [ourName]))
@@ -545,12 +545,12 @@
     unless (null stack) $ do
         let (st',pm) = head stack
         modify $ \ps -> ps {psCurrentState=st', psGameStateMoveStack = tail stack,
-            psLastAlerts = [], psUndoneStack = (st,pm):ustms}
+            psLastAlerts = [], psUndoneStack = pm:ustms}
 processCommand' IMPlay CmdRedo = do
     ustms <- gets psUndoneStack
     case ustms of
         [] -> return ()
-        ustm@(_,pm):ustms' -> do
+        pm:ustms' -> do
             st <- gets psCurrentState
             (st',alerts) <- lift $ doPhysicsTick pm st
             pushPState (st',pm)
@@ -654,8 +654,12 @@
     modify $ \es -> es {selectedPiece = Nothing}
     mpath <- gets esPath
     st <- gets esGameState
+    curSoln <- runMaybeT $ do
+        (st', soln) <- MaybeT $ gets esTested
+        guard $ st' == st
+        return soln
     void.runMaybeT $ do
-        soln <- solveLock (frame,st) $ Just $ "testing " ++ fromMaybe "[unnamed lock]" mpath
+        soln <- solveLock (frame,st) curSoln $ Just $ "testing " ++ fromMaybe "[unnamed lock]" mpath
         lift $ modify $ \es -> es { esTested = Just (st, soln) }
 processCommand' IMEdit CmdUndo = do
     st <- gets esGameState
@@ -704,9 +708,18 @@
             | pos' <- [dir+^pos, pos] ]
 processCommand' IMEdit (CmdRotate _ dir) = do
     selPiece <- gets selectedPiece
+    st <- gets esGameState
     case selPiece of
         Nothing -> return ()
-        Just p  -> doForce $ Torque p dir
+        Just p  ->
+            let PlacedPiece _ pc = getpp st p
+                torquable = case pc of
+                    Hook _ _ -> True
+                    Pivot _  -> True
+                    _        -> False
+            in if torquable
+                then doForce $ Torque p dir
+                else adjustSpringTension p dir
 processCommand' IMEdit (CmdTile tile) = do
     selPos <- gets selectedPos
     drawTile selPos (Just tile) False
@@ -786,15 +799,10 @@
 -- replay it to authenticate as the user.
 encryptPassword :: UIMonad uiM =>
     PublicKey -> String -> String -> MaybeT (MainStateT uiM) String
-encryptPassword publicKey name password = msum
-    [ MaybeT . liftIO .
-        handle (\(e :: SomeException) -> return Nothing) $ do
+encryptPassword publicKey name password = MaybeT . liftIO .
+            handle (\(e :: SomeException) -> return Nothing) $ do
         Right c <- encrypt (defaultOAEPParams SHA256) publicKey . CS.pack $ hashed
         return . Just . CS.unpack $ c
-    , confirmOrBail
-        "Failed to encrypt password - send unencrypted?"
-        >> return hashed
-    ]
     where hashed = hash $ "IY" ++ name ++ password
 
 setSelectedPosFromMouse :: UIMonad uiM => MainStateT uiM ()
@@ -807,12 +815,13 @@
 
 subPlay :: UIMonad uiM => Lock -> MainStateT uiM ()
 subPlay lock =
-    pushEState . psCurrentState =<< execSubMainState (newPlayState lock Nothing Nothing True False)
+    pushEState . psCurrentState =<< execSubMainState (newPlayState lock [] Nothing Nothing True False)
 
-solveLock :: UIMonad uiM => Lock -> Maybe String -> MaybeT (MainStateT uiM) Solution
+solveLock :: UIMonad uiM => Lock -> Maybe Solution -> Maybe String -> MaybeT (MainStateT uiM) Solution
 solveLock = solveLock' Nothing
-solveLock' tutLevel lock title = do
-    (InteractSuccess solved, ps) <- lift $ runSubMainState $ newPlayState (reframe lock) title tutLevel False False
+solveLock' tutLevel lock mSoln title = do
+    (InteractSuccess solved, ps) <- lift $ runSubMainState $
+        newPlayState (reframe lock) (fromMaybe [] mSoln) title tutLevel False False
     guard solved
     return . reverse $ snd <$> psGameStateMoveStack ps
 
@@ -820,7 +829,7 @@
 solveLockSaving ls msps tutLevel lock title = do
     let isTut = isJust tutLevel
     (InteractSuccess solved, ps) <- lift $ runSubMainState $
-        maybe newPlayState restorePlayState msps (reframe lock) title tutLevel False True
+        maybe newPlayState restorePlayState msps (reframe lock) [] title tutLevel False True
     if solved
         then do
             unless isTut . lift . modify $ \ms -> ms { partialSolutions = Map.delete ls $ partialSolutions ms }
diff --git a/InteractUtil.hs b/InteractUtil.hs
--- a/InteractUtil.hs
+++ b/InteractUtil.hs
@@ -26,6 +26,7 @@
 import           Command
 import           EditGameState
 import           Frame
+import           GameState
 import           GameStateTypes
 import           Hex
 import           InputMode
@@ -66,6 +67,17 @@
         let from' = hexVec2HexDirOrZero (to-^from) +^ from
         when (inEditable frame from') $ drawTile from' tile True
         paintTilePath frame tile from' to
+
+adjustSpringTension :: UIMonad uiM => PieceIdx -> Int -> MainStateT uiM ()
+adjustSpringTension p dl = do
+    st <- gets esGameState
+    let updateConn c@(Connection r e@(p',_) (Spring d l))
+            | p' == p
+            , let c' = Connection r e (Spring d $ l + dl)
+            , springExtensionValid st c'
+            = c'
+            | otherwise = c
+    pushEState $ st { connections = updateConn <$> connections st }
 
 pushEState :: UIMonad uiM => GameState -> MainStateT uiM ()
 pushEState st = do
diff --git a/Intricacy.hs b/Intricacy.hs
--- a/Intricacy.hs
+++ b/Intricacy.hs
@@ -40,7 +40,6 @@
 #else
     (Nothing :: (Maybe (CursesUI.UIM MainState -> IO (Maybe MainState))))
 #endif
-
 #ifdef MAIN_CURSES
     (Just (doUI::CursesUI.UIM MainState -> IO (Maybe MainState)))
 #else
diff --git a/MainState.hs b/MainState.hs
--- a/MainState.hs
+++ b/MainState.hs
@@ -40,6 +40,7 @@
 import           Hex
 import           InputMode
 import           Lock
+import           Maxlocksize
 import           Metagame
 import           Mundanities
 import           Physics
@@ -87,7 +88,7 @@
         , wrenchSelected       :: Bool
         , psSolved             :: Bool
         , psGameStateMoveStack :: [(GameState, PlayerMove)]
-        , psUndoneStack        :: [(GameState, PlayerMove)]
+        , psUndoneStack        :: [PlayerMove]
         , psTitle              :: Maybe String
         , psTutLevel           :: Maybe Int
         , psIsSub              :: Bool
@@ -154,7 +155,7 @@
     InitState {}   -> IMInit
     MetaState {}   -> IMMeta
 
-newPlayState (frame,st) title tutLevel sub saved = PlayState st frame [] False False [] [] title tutLevel sub saved Map.empty
+newPlayState (frame,st) pms title tutLevel sub saved = PlayState st frame [] False False [] pms title tutLevel sub saved Map.empty
 newReplayState st soln title = ReplayState st [] soln [] title Map.empty
 newEditState (frame,st) msoln mpath = EditState st [] [] frame mpath
     ((st,)<$>msoln) (Just (st, isJust msoln)) Nothing (PHS zero) (PHS zero) Map.empty
@@ -187,8 +188,8 @@
 initTutProgress = TutProgress False 1 Nothing
 
 wrenchOnlyTutLevel, noUndoTutLevel :: Maybe Int -> Bool
-wrenchOnlyTutLevel = (`elem` (Just <$> [1..3]))
-noUndoTutLevel = (`elem` (Just <$> [1..6]))
+wrenchOnlyTutLevel = (`elem` (Just <$> [1..4]))
+noUndoTutLevel = (`elem` (Just <$> [1..7]))
 
 data InitLock = InitLock
     { initLockName    :: String
@@ -216,12 +217,12 @@
 savePlayState ps = SavedPlayState (getMoves ps) $ Map.map getMoves $ psMarks ps
     where getMoves = reverse . map snd . psGameStateMoveStack
 
-restorePlayState :: SavedPlayState -> Lock -> Maybe String -> Maybe Int -> Bool -> Bool -> MainState
-restorePlayState (SavedPlayState pms markPMs) (frame,st) title tutLevel sub saved =
+restorePlayState :: SavedPlayState -> Lock -> [PlayerMove] -> Maybe String -> Maybe Int -> Bool -> Bool -> MainState
+restorePlayState (SavedPlayState pms markPMs) (frame,st) redoPms title tutLevel sub saved =
     (stateAfterMoves pms) { psMarks = Map.map stateAfterMoves markPMs }
     where
         stateAfterMoves pms = let (stack,st') = applyMoves st pms
-            in (newPlayState (frame, st') title tutLevel sub saved) { psGameStateMoveStack = stack }
+            in (newPlayState (frame, st') redoPms title tutLevel sub saved) { psGameStateMoveStack = stack }
         applyMoves st pms = foldl tick ([],st) pms
         tick :: ([(GameState,PlayerMove)],GameState) -> PlayerMove -> ([(GameState,PlayerMove)],GameState)
         tick (stack,st) pm = ((st,pm):stack,fst . runWriter $ physicsTick pm st)
@@ -250,7 +251,7 @@
             desc <- MaybeT $ listToMaybe <$> readStrings (initDataDir </> name ++ ".text")
             lock <- (fst <$>) . MaybeT $ readLock (initDataDir </> name ++ ".lock")
             solved <- lift . (fromMaybe False <$>) . readReadFile $ initConfDir </> name ++ ".solved"
-            partial <- lift . readReadFile $ initConfDir </> name ++ ".partial"
+            partial <- lift . (fromMaybe Nothing <$>) . readReadFile $ initConfDir </> name ++ ".partial"
             return $ InitLock name desc lock solved partial
     initLocks <- Map.mapMaybe id <$> mapM readInitLock namesMap
     return (tut,initLocks)
@@ -288,19 +289,26 @@
     writeServerSolns saddr ms
 writeMetaState _ = return ()
 
-getTitle :: UIMonad uiM => MainStateT uiM (Maybe String)
+getTitle :: UIMonad uiM => MainStateT uiM (Maybe (String, Int))
 getTitle = get >>= title . ms2im
     where
     title 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 "    ")
-    title IMPlay = gets psTitle
-    title IMReplay = gets rsTitle
+        height <- gets $ aboveFrame . esFrame
+        return $ Just ("editing " ++ fromMaybe "[unnamed lock]" mpath ++
+                (if isTested then " (Tested)" else "") ++
+                (if unsaved then " [+]" else "    "),
+            height
+            )
+    title IMPlay = do
+        height <- gets $ aboveFrame . psFrame
+        gets $ ((, height) <$>) . psTitle
+    title IMReplay = gets $ ((, maxHeight) <$>) . rsTitle
     title _ = return Nothing
+    aboveFrame frame = min maxHeight $ 2 + frameSize frame
+    maxHeight = maxlocksize + 1
 
 editStateUnsaved :: UIMonad uiM => MainStateT uiM Bool
 editStateUnsaved = (isNothing <$>) $ runMaybeT $ do
@@ -502,18 +510,17 @@
     guard $ ourName /= name
     uinfo <- mgetUInfo name
     ourUInfo <- mgetUInfo ourName
-    let (neg,pos) = (countPoints ourUInfo uinfo, countPoints uinfo ourUInfo)
+    let (pos,neg) = (countUnaccessedBy ourUInfo name, countUnaccessedBy uinfo ourName)
     return (pos-neg,(pos,neg))
     where
-        countPoints mugu masta = length $ filter (maybe False winsPoint) $ getAccessInfo mugu masta
+        countUnaccessedBy ui name = length $ filter isNothing $ getAccessInfo ui name
 
 accessedAL :: (UIMonad uiM) => ActiveLock -> MainStateT uiM Bool
 accessedAL (ActiveLock name idx) = (isJust <$>) $ runMaybeT $ do
     ourName <- mgetOurName
     guard $ ourName /= name
     uinfo <- mgetUInfo name
-    ourUInfo <- mgetUInfo ourName
-    guard $ isJust $ getAccessInfo uinfo ourUInfo !! idx
+    guard $ isJust $ getAccessInfo uinfo ourName !! idx
 
 getNotesReadOn :: UIMonad uiM => LockInfo -> MainStateT uiM [NoteInfo]
 getNotesReadOn lockinfo = (fromMaybe [] <$>) $ runMaybeT $ do
@@ -561,8 +568,7 @@
     , ""
     , "The game judges players relative to each of their peers. There are no absolute rankings."
     , "You win a point of esteem against another player for one of their locks"
-    , "if you have solved the lock and declared a note which remains unread by the lock's owner,"
-    , "or if you have read three notes by other players on the lock."
+    , "if you have declared a solution to it, or if you have read three notes on it."
     , "You also win a point for each empty lock slot if you can unlock all full slots."
     , "Relative esteem is the points you win minus the points they win; +3 is best, -3 is worst."
     , ""
diff --git a/Metagame.hs b/Metagame.hs
--- a/Metagame.hs
+++ b/Metagame.hs
@@ -39,43 +39,21 @@
 
 initLockInfo ls = LockInfo ls False [] [] []
 
-data NoteInfo = NoteInfo {noteAuthor::Codename, noteBehind::Maybe ActiveLock, noteOn::ActiveLock}
-    deriving (Eq, Ord, Show, Read)
-
-data ActiveLock = ActiveLock {lockOwner::Codename, lockIndex :: LockIndex}
+data AccessedReason = AccessedPrivy | AccessedEmpty | AccessedPub
     deriving (Eq, Ord, Show, Read)
 
-data AccessedReason = AccessedPrivyRead | AccessedPrivySolved {noteReadByOwner::Bool} | AccessedEmpty | AccessedPub
-    deriving (Eq, Ord, Show, Read)
-winsPoint :: AccessedReason -> Bool
-winsPoint (AccessedPrivySolved True) = False
-winsPoint _                          = True
-
-getAccessInfo :: UserInfo -> UserInfo -> [Maybe AccessedReason]
-getAccessInfo accessedUInfo accessorUInfo =
-    let accessor = codename accessorUInfo
-        mlinfos = elems $ userLocks accessedUInfo
+getAccessInfo :: UserInfo -> Codename -> [Maybe AccessedReason]
+getAccessInfo ui name =
+    let mlinfos = elems $ userLocks ui
         accessedSlot = maybe accessedAllExisting accessedLock
         accessedAllExisting = all (maybe True accessedLock) mlinfos
-        accessedLock linfo = public linfo || accessor `elem` accessedBy linfo
+        accessedLock linfo = public linfo || name `elem` accessedBy linfo
     in map (maybe
             (if accessedAllExisting then Just AccessedEmpty else Nothing)
-            (\linfo -> if public linfo then Just AccessedPub else
-                if not $ accessedLock linfo then Nothing else Just $
-                    if countRead accessorUInfo linfo >= notesNeeded then AccessedPrivyRead
-                        else AccessedPrivySolved $ not.null $
-                            [ n
-                            | n <- lockSolutions linfo
-                            , noteAuthor n == accessor
-                            , isNothing (noteBehind n) || n `elem` notesRead accessedUInfo ]))
+            (\linfo -> if public linfo then Just AccessedPub
+                else if accessedLock linfo then Just AccessedPrivy else Nothing))
         mlinfos
 
-countRead :: UserInfo -> LockInfo -> Int
-countRead reader tlock = fromIntegral $ length
-        $ filter (\n -> (isNothing (noteBehind n) || n `elem` notesRead reader)
-            && noteAuthor n /= codename reader)
-        $ lockSolutions tlock
-
 data UserInfoDelta
     = AddRead NoteInfo
     | DelRead NoteInfo
@@ -89,6 +67,12 @@
     | 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
diff --git a/Mundanities.hs b/Mundanities.hs
--- a/Mundanities.hs
+++ b/Mundanities.hs
@@ -13,7 +13,7 @@
 import           Control.Arrow
 import           Control.Monad
 import           Control.Monad.Catch    (MonadMask, catch, handle)
-import           Control.Monad.IO.Class (MonadIO)
+import           Control.Monad.IO.Class (MonadIO, liftIO)
 import qualified Data.ByteString        as BS
 import qualified Data.ByteString.Char8  as BSC
 import           Data.List
@@ -31,6 +31,9 @@
 
 ignoreIOErrAlt :: (MonadIO m, MonadMask m, Alternative f) => m (f a) -> m (f a)
 ignoreIOErrAlt = handle ((\_ -> return empty) :: (Monad m, Alternative f) => IOError -> m (f a))
+
+warnIOErrAlt :: (MonadIO m, MonadMask m, Alternative f) => m (f a) -> m (f a)
+warnIOErrAlt = handle ((\e -> liftIO (print e) >> return empty) :: (MonadIO m, Alternative f) => IOError -> m (f a))
 
 unlessIOErr :: (MonadIO m, MonadMask m) => m Bool -> m Bool
 unlessIOErr = (fromMaybe False <$>) . ignoreIOErrAlt . (Just <$>)
diff --git a/NEWS b/NEWS
--- a/NEWS
+++ b/NEWS
@@ -1,5 +1,15 @@
 This is an abbreviated summary; see the git log for gory details.
 
+0.8.1:
+    Revise tutorial and initiation levels REP and GAP
+    Disallow overextended/compressed springs
+    Allow editing spring tension by turning
+    Add compact solution format
+    Revert scoring change introduced in 0.6
+    Add --datadir option
+    Fix loading partial initiation solutions
+    Fix server locking
+
 0.8:
     Add single-player introductory subgame ("initiation").
     Improve UI variously.
diff --git a/Physics.hs b/Physics.hs
--- a/Physics.hs
+++ b/Physics.hs
@@ -135,10 +135,15 @@
                             then (f /= f',[])
                             else let cols = collisions st' (forceIdx f) (forceIdx f')
                                 in (not $ null cols, cols) ]]
+                badConns = [ c | c <- connections st'
+                    , not $ springExtensionValid st' c ]
             in do
                 tell $ map AlertBlockingForce $ concat inconsistencies
                 tell $ map AlertCollision cols
-                return $ if null inconsistencies then []
+                tell $ map AlertCollision $ concat
+                    [ locusPos st' <$> [r, e]
+                    | Connection r e _ <- badConns]
+                return $ if null inconsistencies && null badConns then []
                     else if i==j then [i]
                     else if dominates i j then [j]
                     else if dominates j i then [i]
@@ -240,8 +245,8 @@
     bumps ++ springTransmissions
     where
         idx = forceIdx force
-        bumps = [ ForceChoice $ map (transmittedForce st idx' cpos) dirs |
-            idx' <- ppidxs st
+        bumps = [ ForceChoice $ map (transmittedForce st idx' cpos) dirs
+            | idx' <- ppidxs st
             , idx' /= idx
             , cpos <- collisionsWithForce st force idx'
             , let dirs = case force of
@@ -252,8 +257,8 @@
                               arm = cpos -^ placedPos (getpp st idx) ]
         springTransmissions =
             case force of
-                 Push _ dir -> [ ForceChoice [Push idx' dir] |
-                        c@(Connection (ridx,_) (eidx,_) (Spring sdir _)) <- conns
+                 Push _ dir -> [ ForceChoice [Push idx' dir]
+                        | c@(Connection (ridx,_) (eidx,_) (Spring sdir _)) <- conns
                         , let root = idx == ridx
                         , let end = idx == eidx
                         , root || end
diff --git a/README b/README
--- a/README
+++ b/README
@@ -42,18 +42,17 @@
 the bit with the 3-letter codenames and the three lock slots and notes and so
 on. If you haven't seen anything of the kind... keep solving those locks!)
 
-A player accesses a lock slot when one of the following holds
-    (i) the player has read three notes on the lock in the slot;
-    (ii) the player has solved the lock in the slot, and declared the solution;
-    (iii) there's no lock in the slot, and the player has accessed all the slots
-	which do have locks in them.
-
 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; but a point is not awarded for
-case (ii) if the owner of the lock has read the note.
+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.
@@ -123,8 +122,9 @@
 	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.
+        compress resp. extend the spring and the spring is currently extended 
+        resp. compressed, or the force is a player force and the spring isn't 
+        already fully compressed resp. 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
@@ -142,7 +142,8 @@
 
 Two forces are _inconsistent_ when
     * they act on the same piece in different directions, or
-    * applying both at once results in overlapping pieces.
+    * applying both at once results in overlapping pieces, or
+    * applying both at once results in an overextended/overcompressed spring.
 
 Two force groups are _inconsistent_ iff there is some force from the first
     which is inconsistent with some force from the second.
diff --git a/SDLUI.hs b/SDLUI.hs
--- a/SDLUI.hs
+++ b/SDLUI.hs
@@ -211,7 +211,8 @@
 
 data AccessedInfo = AccessedSolved | AccessedPublic | AccessedReadNotes | AccessedUndeclared
     deriving (Eq, Ord, Show)
-data Selectable = SelOurLock
+data Selectable
+    = SelOurLock
     | SelTut Bool
     | SelInitLock HexVec Bool
     | SelLock ActiveLock
@@ -234,7 +235,7 @@
     | SelPrivyHeader
     | SelNotesHeader
     | SelToolHook
-    | SelToolWrench
+    | SelToolWrench Bool
     deriving (Eq, Ord, Show)
 
 registerSelectable :: HexVec -> Int -> Selectable -> UIM ()
@@ -249,9 +250,9 @@
 clearSelectables = modify $ \ds -> ds {registeredSelectables = Map.empty}
 clearButtons = modify $ \ds -> ds {contextButtons = []}
 
-registerUndoButtons :: Bool -> Bool -> UIM ()
-registerUndoButtons noUndo noRedo = do
-    unless noUndo $ registerButton (periphery 2+^hu) CmdUndo 0 [("undo",hu+^neg hw)]
+registerUndoButtons :: Bool -> UIM ()
+registerUndoButtons noRedo = do
+    registerButton (periphery 2+^hu) CmdUndo 0 [("undo",hu+^neg hw)]
     unless noRedo $ registerButton (periphery 2+^hu+^neg hv) CmdRedo 2 [("redo",hu+^neg hw)]
 
 commandOfSelectable IMInit (SelTut _) _ = Just . CmdSolveInit $ Just zero
@@ -310,24 +311,16 @@
     "Random set of players. Colours show relative esteem, bright red (-3) to bright green (+3)."
 helpOfSelectable (SelScoreLock (Just name) Nothing _) = Just $
     "Your lock. "++name++" can not not unlock it."
-helpOfSelectable (SelScoreLock (Just name) (Just AccessedPrivyRead) _) = Just $
-    "Your lock. "++name++" has read three notes on it: -1 to relative esteem."
-helpOfSelectable (SelScoreLock (Just name) (Just (AccessedPrivySolved False)) _) = Just $
-    "Your lock. "++name++" has declared a note on it which you have not read: -1 to relative esteem."
-helpOfSelectable (SelScoreLock (Just name) (Just (AccessedPrivySolved True)) _) = Just $
-    "Your lock. "++name++" has declared a note on it, but you have read that note."
+helpOfSelectable (SelScoreLock (Just name) (Just AccessedPrivy) _) = Just $
+    "Your lock. "++name++" can unlock it: -1 to relative esteem."
 helpOfSelectable (SelScoreLock (Just name) (Just AccessedPub) _) = Just
     "Your lock. Its secrets have been publically revealed: -1 to relative esteem."
 helpOfSelectable (SelScoreLock (Just name) (Just AccessedEmpty) _) = Just $
     "Your empty lock slot. "++name++" can unlock all your locks: -1 to relative esteem."
 helpOfSelectable (SelScoreLock Nothing Nothing (ActiveLock name _)) = Just $
     name++"'s lock. You can not unlock it."
-helpOfSelectable (SelScoreLock Nothing (Just AccessedPrivyRead) (ActiveLock name _)) = Just $
-    name++"'s lock. You have read three notes on it: +1 to relative esteem."
-helpOfSelectable (SelScoreLock Nothing (Just (AccessedPrivySolved False)) (ActiveLock name _)) = Just $
-    name++"'s lock. You have declared a note on it which "++name++" has not read: +1 to relative esteem."
-helpOfSelectable (SelScoreLock Nothing (Just (AccessedPrivySolved True)) (ActiveLock name _)) = Just $
-    name++"'s lock. You have declared a note on it, but "++name++" has read your note."
+helpOfSelectable (SelScoreLock Nothing (Just AccessedPrivy) (ActiveLock name _)) = Just $
+    name++"'s lock. You can unlock it: +1 to relative esteem."
 helpOfSelectable (SelScoreLock Nothing (Just AccessedPub) (ActiveLock name _)) = Just $
     name++"'s lock. Its secrets have been publically revealed: +1 to relative esteem."
 helpOfSelectable (SelScoreLock Nothing (Just AccessedEmpty) (ActiveLock name _)) = Just $
@@ -360,7 +353,8 @@
     "Notes on this lock declared by players who picked the lock."
 helpOfSelectable SelNotesHeader = Just
     "Notes secured by this lock. These notes are read by everyone who can unlock the lock."
-helpOfSelectable SelToolWrench = Just "The wrench, one of your lockpicking tools. Click and drag to move."
+helpOfSelectable (SelToolWrench False) = Just "The wrench, one of your lockpicking tools. Click and drag to move."
+helpOfSelectable (SelToolWrench True) = Just "The wrench, currently in motion. It will keep moving until it is blocked."
 helpOfSelectable SelToolHook = Just "The hook, one of your lockpicking tools. Click and drag to move, use mousewheel to turn."
 
 cmdAtMousePos pos@(mPos,central) im selMode = do
@@ -523,8 +517,11 @@
 -- ^ 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
 
+-- XXX HACK: the n=8 line is crowded
+titlePos n = titlePos' $ if n == 8 then 9 else n
+    where titlePos' n = n*^hv +^ (n`div`2)*^hu
+
 screenWidthHexes,screenHeightHexes :: Int
 screenWidthHexes = 32
 screenHeightHexes = 26
@@ -904,7 +901,7 @@
 clearMsg :: UIM ()
 clearMsg = modify $ \s -> s { message = Nothing }
 
-drawTitle (Just title) = renderToMain $ renderStrColAtCentre messageCol title titlePos
+drawTitle (Just (title,n)) = renderToMain $ renderStrColAtCentre messageCol title (titlePos n)
 drawTitle Nothing      = return ()
 
 say = setMsgLine messageCol
diff --git a/SDLUIMInstance.hs b/SDLUIMInstance.hs
--- a/SDLUIMInstance.hs
+++ b/SDLUIMInstance.hs
@@ -100,12 +100,12 @@
     getUIBinding mode cmd = ($cmd) <$> getBindingStr mode
 
     initUI = (isJust <$>) . runMaybeT $ do
-        catchIOErrorMT $ SDL.init
+        let toInit = [InitVideo]
 #ifdef SOUND
-            [InitVideo,InitAudio]
-#else
-            [InitVideo]
+                ++ [InitAudio]
 #endif
+        catchIOErrorMT $ SDL.init toInit
+        liftIO (SDL.wasInit [InitVideo]) >>= guard . (InitVideo `elem`)
         catchIOErrorMT TTF.init
         lift $ do
             readUIConfigFile
@@ -118,7 +118,7 @@
             initAudio
             readBindings
         where
-            catchIOErrorMT m = MaybeT . liftIO . ignoreIOErrAlt $ m >> return (Just ())
+            catchIOErrorMT m = MaybeT . liftIO . warnIOErrAlt $ m >> return (Just ())
 
     endUI = do
         writeUIConfigFile
@@ -444,7 +444,6 @@
 drawMainState' :: MainState -> MainStateT UIM ()
 drawMainState' PlayState { psCurrentState=st, psLastAlerts=alerts,
         wrenchSelected=wsel, psTutLevel=tutLevel, psSolved=solved } = do
-    canUndo <- gets (null . psGameStateMoveStack)
     canRedo <- gets (null . psUndoneStack)
     let isTut = isJust tutLevel
     lift $ do
@@ -458,27 +457,28 @@
             centre <- gets dispCentre
             sequence_
                 [ registerSelectable (pos -^ centre) 0 $
-                   if isWrench p then SelToolWrench else SelToolHook
+                    case p of
+                        Wrench v -> SelToolWrench $ v /= zero
+                        _        -> SelToolHook
                 | not $ lb || rb
                 , PlacedPiece pos p <- Vector.toList $ placedPieces st
                 , isTool p]
         unless (noUndoTutLevel tutLevel) $ do
-            registerUndoButtons canUndo canRedo
+            registerUndoButtons canRedo
             registerButtonGroup markButtonGroup
         registerButton (periphery 0) CmdOpen (if solved then 2 else 0) $
             ("open", hu+^neg hw) : [("Press-->",9*^neg hu) | solved && isTut]
 drawMainState' ReplayState { rsCurrentState=st, rsLastAlerts=alerts } = do
-    canUndo <- gets (null . rsGameStateMoveStack)
     canRedo <- gets (null . rsMoveStack)
     lift $ do
         drawMainGameState [] False alerts st
-        registerUndoButtons canUndo canRedo
+        registerUndoButtons canRedo
         renderToMain $ drawCursorAt Nothing
 drawMainState' EditState { esGameState=st, esGameStateStack=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)
+    registerUndoButtons (null undostack)
     when (isJust selPiece) $ mapM_ registerButtonGroup
         [ singleButton (periphery 2 +^ 3*^hw+^hv) CmdDelete 0 [("delete",hu+^neg hw)]
         , singleButton (periphery 2 +^ 3*^hw) CmdMerge 1 [("merge",hu+^neg hw)]
@@ -620,7 +620,7 @@
         us <- liftMaybe ourName
         ourUInfo <- mgetUInfo us
         selUInfo <- mgetUInfo sel
-        let accesses = map (uncurry getAccessInfo) [(ourUInfo,selUInfo),(selUInfo,ourUInfo)]
+        let accesses = map (uncurry getAccessInfo) [(ourUInfo,sel),(selUInfo,us)]
         let posLeft = scoresPos +^ hw +^ neg hu
         let posRight = posLeft +^ 3*^hu
         size <- snd <$> (lift.lift) getGeom
@@ -635,7 +635,7 @@
                 , let accessed = head accesses !! i
                 , let col
                         | accessed == Just AccessedPub = dim pubColour
-                        | maybe False winsPoint accessed = dim $ scoreColour $ -3
+                        | isJust accessed = dim $ scoreColour $ -3
                         | otherwise = obscure $ scoreColour 3 ]
             fillArea (posRight+^hw) (map (posRight+^) [zero,hw,neg hv])
                 [ \pos -> lift (registerSelectable pos 0 (SelScoreLock Nothing accessed $ ActiveLock sel i)) >>
@@ -644,7 +644,7 @@
                 , let accessed = accesses !! 1 !! i
                 , let col
                         | accessed == Just AccessedPub = obscure pubColour
-                        | maybe False winsPoint accessed = dim $ scoreColour 3
+                        | isJust accessed = dim $ scoreColour 3
                         | otherwise = obscure $ scoreColour $ -3 ]
         (posScore,negScore) <- MaybeT $ (snd<$>) <$> getRelScoreDetails sel
         lift.lift $ sequence_
diff --git a/Server.hs b/Server.hs
--- a/Server.hs
+++ b/Server.hs
@@ -56,8 +56,9 @@
 
 import qualified Crypto.Argon2              as A2
 import           Crypto.Hash.Algorithms     (SHA256 (..))
-import           Crypto.PubKey.RSA          (generate)
+import           Crypto.PubKey.RSA          (generate, generateBlinder)
 import           Crypto.PubKey.RSA.OAEP     (decrypt, defaultOAEPParams)
+import           Crypto.PubKey.RSA.Types    (private_n)
 
 import           Network.Mail.Mime          (plainPart)
 import qualified Network.Mail.SMTP          as SMTP
@@ -224,13 +225,17 @@
             GetRandomNames _ -> ReadMode
             _                -> ReadWriteMode
 
-    -- check solutions prior to write-locking database:
-    withDBLock dbpath ReadMode (runExceptT checkRequest) >>=
-        either (return . ServerError) (const $
-        withDBLock dbpath lockMode $ runExceptT handleRequest' >>=
-            either (return . ServerError) return)
+    -- Check solutions prior to write-locking database.
+    -- Slightly awkward, because we have to drop the read lock before
+    -- acquiring the write lock, so need to check preconditions again once we
+    -- have the write lock.
+    withDBLock dbpath ReadMode (runExceptT $ checkRequest Nothing) >>=
+        either (return . ServerError) (\mCheckedLock ->
+            withDBLock dbpath lockMode $
+                runExceptT (checkRequest mCheckedLock >> handleRequest') >>=
+                    either (return . ServerError) return)
     where
-        checkRequest = do
+        checkRequest mCheckedLock = do
             when (pv /= protocolVersion) $ throwE "Bad protocol version"
             case action of
                 DeclareSolution soln ls target idx -> do
@@ -247,7 +252,10 @@
                         throwE "That's your lock!"
                     behindLock <- getALock behind
                     when (public behindLock) $ throwE "Your lock is cracked!"
-                    unless (checkSolution lock soln) $ throwE "Bad solution"
+                    case mCheckedLock of
+                        Nothing -> unless (checkSolution lock soln) $ throwE "Bad solution"
+                        Just lock' -> unless (lock == lock') $ throwE "Lock changed!"
+                    return $ Just lock
                 SetLock lock@(frame,_) idx soln -> do
                     ServerInfo serverSize _ <- getServerInfo
                     when (frame /= BasicFrame serverSize) $ throwE $
@@ -258,8 +266,11 @@
                             `catchE` const (return (RCLockHashes []))
                     let hashed = hash $ show lock
                     when (hashed `elem` hashes) $ throwE "Lock has already been used"
-                    unless (checkSolution lock soln) $ throwE "Bad solution"
-                _ -> return ()
+                    case mCheckedLock of
+                        Nothing -> unless (checkSolution lock soln) $ throwE "Bad solution"
+                        Just lock' -> unless (lock == lock') $ throwE "Lock changed!"
+                    return $ Just lock
+                _ -> return Nothing
         handleRequest' =
             case action of
                 UndefinedAction -> throwE "Request not recognised by this server"
@@ -406,9 +417,13 @@
         decryptPassword :: String -> ExceptT String IO String
         decryptPassword pw = do
             RCSecretKey secretKey <- getRecordErrored RecSecretKey
-            ExceptT . return . bimap show CS.unpack .
-                decrypt Nothing (defaultOAEPParams SHA256) secretKey . CS.pack $ pw
-                -- <=intricacy-0.6.2 sends the hashed password unencrypted
+            blinder <- liftIO . generateBlinder $ private_n secretKey
+            ExceptT . return . bimap
+                    (\err -> show err ++ "; try deleting ~/.intricacy/cache ?")
+                    CS.unpack .
+                decrypt (Just blinder) (defaultOAEPParams SHA256) secretKey . CS.pack $ pw
+            -- XXX: <=intricacy-0.6.2 sends the hashed password unencrypted,
+            -- but we don't support that anymore
         convertLegacyPW :: Codename -> IO ()
         convertLegacyPW name = void . runExceptT $ do
             RCPasswordLegacy legacyPw <- getRecordErrored (RecPasswordLegacy name)
@@ -508,7 +523,9 @@
             info <- getCurrUserInfo name
             tlock <- getCurrALock target
             unless (name `elem` accessedBy tlock || public tlock || name == lockOwner target) $ do
-                when (countRead info tlock == notesNeeded) $
+                let countRead = fromIntegral $ length $
+                        filter (\n -> isNothing (noteBehind n) || n `elem` notesRead info) $ lockSolutions tlock
+                when (countRead == notesNeeded) $
                     accessLock name target tlock
         checkSuffPubNotes al@(ActiveLock name idx) = do
             lock <- getCurrALock al
diff --git a/Version.hs b/Version.hs
--- a/Version.hs
+++ b/Version.hs
@@ -11,4 +11,4 @@
 module Version where
 
 version :: String
-version = "0.8.0.1"
+version = "0.8.1"
diff --git a/initiation/GAP.lock b/initiation/GAP.lock
--- a/initiation/GAP.lock
+++ b/initiation/GAP.lock
@@ -1,17 +1,17 @@
-        & & & & & & & & &          
-       &                 & & & &   
-      &             % %   &     &  
-     & # C C C C % %   %   & #   & 
-    &                 % # # #     &
-   &                 %   7   &   & 
-  & $ $ S S S " O O %   7     & &  
- &             #   " % %       &   
-& $ S S S S S %   " C C C C C D &  
- &         # %   % O           &   
-  &             % Z           &    
- & &           %   Z         &     
-& * '             # #       &      
- & @ &         # #         &       
-  & & &                   &        
-       &                 &         
-        & & & & & & & & &          
+        " " " " " " " " "          
+       "                 " " " "   
+      " & C C C C % % %   "     "  
+     " & C C C C % % % %   " #   " 
+    "       #   % % % % # # #     "
+   "           o -   %   7   "   " 
+  "             `   %   7     " "  
+ " % % % % % % %   # % %       "   
+" $ S S S S S #   # C C C C C D "  
+ " % % % % % #   % O           "   
+  "             % Z           "    
+ " "           %   Z         "     
+" * '             # #       "      
+ " @ "         # #         "       
+  " " "                   "        
+       "                 "         
+        " " " " " " " " "          
diff --git a/initiation/REP.lock b/initiation/REP.lock
--- a/initiation/REP.lock
+++ b/initiation/REP.lock
@@ -1,17 +1,17 @@
         " " " " " " " " "          
-       " } #             " " " "   
-      " & }   # # S S %   "     "  
-     "   & }       & & % % "     " 
-    "     & }     &       % % %   "
-   " %     & }   &           "   " 
-  " [ % $ # & } &             " "  
- " [         & &           o   "   
-" # # # # # o - O         / ` % "  
- "   % #   /     O       %   % "   
-  " % # %         O     %     "    
- " "   % %         O   %     "     
-" * '     %         O %     "      
- " @ "     % # # # # O   # "       
-  " " "     #             "        
+       " }               " " " "   
+      " & }     # S S %   "     "  
+     " # & }       & & % % "     " 
+    " 9 " & }     &       % % %   "
+   " % 9 # & }   &           "   " 
+  "   % 9 " & } &             " "  
+ "     % 9 # & &           o   "   
+"       % 9 \ ' O         / ` # "  
+ "       %   o   O       #   # "   
+  " % % S # /     O     #     "    
+ " "   %   #       O   #     "     
+" * '     %         O #     "      
+ " @ "     % % % % % O   # "       
+  " " "       %           "        
        "           #     "         
         " " " " " " " " "          
diff --git a/initiation/REP.text b/initiation/REP.text
--- a/initiation/REP.text
+++ b/initiation/REP.text
@@ -1,1 +1,1 @@
-Do the shuffle.
+Do the shuffle
diff --git a/intricacy.cabal b/intricacy.cabal
--- a/intricacy.cabal
+++ b/intricacy.cabal
@@ -1,6 +1,6 @@
 cabal-version:      >=1.10
 name:               intricacy
-version:            0.8.0.1
+version:            0.8.1
 license:            GPL-3
 license-file:       COPYING
 maintainer:         mbays@sdf.org
@@ -103,22 +103,22 @@
         default-extensions: DoAndIfThenElse
         build-depends:
             base >=4.3 && <5,
-            mtl >=2.1.3.1 && <2.3,
-            transformers >=0.3.0.0 && <0.6,
+            mtl >=2.1.3.1 && <2.4,
+            transformers >=0.3.0.0 && <0.8,
             stm >=2.1 && <2.6,
             directory >=1.0 && <1.4,
             exceptions >=0.8.3 && <0.11,
             filepath >=1.0 && <1.5,
-            time >=1.2 && <1.10,
-            bytestring ==0.10.*,
+            time >=1.2 && <1.14,
+            bytestring >=0.10 && <0.12,
             array >=0.3 && <0.6,
             containers >=0.4 && <0.7,
             vector >=0.9 && <0.13,
-            binary >=0.5 && <0.9,
-            network-fancy >=0.1.5 && <0.3,
+            binary >=0.5 && <0.11,
+            network-simple >=0.3 && <0.5,
             safe >=0.3.18 && <0.4,
-            memory >= 0.11 && <0.16,
-            cryptonite >= 0.16 && <0.28
+            memory >=0.11 && <0.18,
+            cryptonite >=0.16 && <0.31
 
         if !impl(ghc >=8.0)
             build-depends: semigroups ==0.18.*
@@ -133,7 +133,7 @@
                 cpp-options:   -DSOUND
                 build-depends:
                     SDL-mixer ==0.6.*,
-                    random >=1.0 && <1.2
+                    random >=1.0 && <1.4
 
             if os(windows)
                 extra-libraries:
@@ -148,21 +148,20 @@
                     extra-libraries: SDL_mixer
 
             if os(osx)
-                cpp-options:   -DAPPLE
-                c-sources:     c_main.c
-                include-dirs:  /usr/include/SDL /usr/local/include/SDL
-                ghc-options:   -no-hs-main
-                build-depends: network-fancy <0.2.1
+                cpp-options:  -DAPPLE
+                c-sources:    c_main.c
+                include-dirs: /usr/include/SDL /usr/local/include/SDL
+                ghc-options:  -no-hs-main
 
         if flag(curses)
-            cpp-options:   -DMAIN_CURSES
+            cpp-options:       -DMAIN_CURSES
+            pkgconfig-depends: ncursesw -any
             other-modules:
                 CursesRender
                 CursesUI
                 CursesUIMInstance
 
-            build-depends: hscurses ==1.4.*
-            pkgconfig-depends: ncursesw
+            build-depends:     hscurses ==1.4.*
 
         if flag(sdl)
             cpp-options:   -DMAIN_SDL
@@ -205,31 +204,31 @@
         default-extensions: DoAndIfThenElse
         build-depends:
             base >=4.3 && <5,
-            mtl >=2.2 && <2.3,
-            transformers >=0.4 && <0.6,
+            mtl >=2.2 && <2.4,
+            transformers >=0.4 && <0.8,
             stm >=2.1 && <2.6,
             directory >=1.0 && <1.4,
             exceptions >=0.8.3 && <0.11,
             filepath >=1.0 && <1.5,
-            time >=1.5 && <1.10,
-            bytestring ==0.10.*,
+            time >=1.5 && <1.14,
+            bytestring >=0.10 && <0.12,
             array >=0.3 && <0.6,
             containers >=0.4 && <0.7,
             vector >=0.9 && <0.13,
-            binary >=0.5 && <0.9,
+            binary >=0.5 && <0.11,
             network-fancy >=0.1.5 && <0.3,
             safe >=0.3.18 && <0.4,
-            random >=1.0 && <1.3,
+            random >=1.0 && <1.4,
             pipes >=4 && <4.4,
             feed >=1.1 && <1.4,
-            xml-conduit >=1.0 && < 1.10,
+            xml-conduit >=1.0 && <1.10,
             email-validate >=1.0 && <2.4,
-            text >=0.1 && < 1.3,
+            text >=0.1 && <1.3,
             text-short ==0.1.*,
-            mime-mail >=0.4.4 && < 0.6,
-            smtp-mail >=0.1.4.1 && < 0.4,
-            memory >= 0.11 && <0.16,
-            cryptonite >= 0.16 && <0.28,
+            mime-mail >=0.4.4 && <0.6,
+            smtp-mail >=0.1.4.1 && <0.4,
+            memory >=0.11 && <0.18,
+            cryptonite >=0.16 && <0.31,
             argon2 ==1.3.*
 
         if !impl(ghc >=8.0)
diff --git a/tutorial/01-winning.lock b/tutorial/01-winning.lock
--- a/tutorial/01-winning.lock
+++ b/tutorial/01-winning.lock
@@ -1,11 +1,11 @@
        " " " " " "       
       "           " " "  
      "             "   " 
-    " S S S S S S # # # "
-   "             #   " " 
-  "                   "  
- " "                 "   
-" * '               "    
- " @ "             "     
-  " " "           "      
+    " S S S S # # # # # "
+   " %       #       " " 
+  " % % % %         % "  
+ " "       % % %   % "   
+" * '             % "    
+ " @ " %         % "     
+  " " " % % % % % "      
        " " " " " "       
diff --git a/tutorial/01-winning.text b/tutorial/01-winning.text
--- a/tutorial/01-winning.text
+++ b/tutorial/01-winning.text
@@ -1,1 +1,1 @@
-drag your tools to pull the bolt aside, then press 'O'
+drag your tools to push the bolt aside, then press 'O'
diff --git a/tutorial/03-time.lock b/tutorial/03-time.lock
new file mode 100644
--- /dev/null
+++ b/tutorial/03-time.lock
@@ -0,0 +1,15 @@
+        & & & & & & & &        
+       &   O           &       
+      &   % % % % % % % & & &  
+     &   %               & ~ & 
+    & " c c c c c c c c # ~   &
+   &   ]               ~ ~ & & 
+  & %   ]             ~ 1   &  
+ &   % % # %     '   ~ 1     & 
+  &     %   %   o   ~ #     &  
+ & &   %   %     ` ~   #   &   
+& * '     %       %   #   &    
+ & @ & % %       %   #   &     
+  & & &           5 #   &      
+       &           #   &       
+        & & & & & & & &        
diff --git a/tutorial/03-time.text b/tutorial/03-time.text
new file mode 100644
--- /dev/null
+++ b/tutorial/03-time.text
@@ -0,0 +1,1 @@
+time passes when you move
diff --git a/tutorial/03-wrench.lock b/tutorial/03-wrench.lock
deleted file mode 100644
--- a/tutorial/03-wrench.lock
+++ /dev/null
@@ -1,13 +0,0 @@
-       " " " " " " "       
-      "     #       " " "  
-     " S S # #   %   "   " 
-    "             % % % % "
-   "   o   # S S S S % " " 
-  "     `             # "  
- "   % % % % % %   # #   " 
-  " %   %               "  
- " "   % % %     #     "   
-" * '     % %         "    
- " @ " %   % o       "     
-  " " " %   /     # "      
-       " " " " " " "       
diff --git a/tutorial/03-wrench.text b/tutorial/03-wrench.text
deleted file mode 100644
--- a/tutorial/03-wrench.text
+++ /dev/null
@@ -1,1 +0,0 @@
-the wrench moves in a straight line until blocked
diff --git a/tutorial/04-hook.lock b/tutorial/04-hook.lock
deleted file mode 100644
--- a/tutorial/04-hook.lock
+++ /dev/null
@@ -1,13 +0,0 @@
-       " " " " " " "       
-      " %   # #   % " " "  
-     " %     Z #   % " # " 
-    " % %   % Z     % #   "
-   " %   # % % Z     # " " 
-  " % %   Z % % Z     # "  
- " % % %   Z % % %     # " 
-  " % % %   % % %   % # "  
- " "   % ]     %   # # "   
-" * '     ]   % %   # "    
- " @ "   # #   % % 7 "     
-  " " "   % % % % 7 "      
-       " " " " " " "       
diff --git a/tutorial/04-hook.text b/tutorial/04-hook.text
deleted file mode 100644
--- a/tutorial/04-hook.text
+++ /dev/null
@@ -1,1 +0,0 @@
-turn hook with mouse wheel to pull springs aside; numpad and vi keys also supported
diff --git a/tutorial/04-wrench.lock b/tutorial/04-wrench.lock
new file mode 100644
--- /dev/null
+++ b/tutorial/04-wrench.lock
@@ -0,0 +1,13 @@
+       " " " " " " "       
+      "     #       " " "  
+     " S S # #   %   "   " 
+    "             % % % % "
+   "   o   # S S S S % " " 
+  "     `             # "  
+ "   % % % % % %   # #   " 
+  " %   %               "  
+ " "   % % %     #     "   
+" * '     % %         "    
+ " @ " %   % o       "     
+  " " " %   /     # "      
+       " " " " " " "       
diff --git a/tutorial/04-wrench.text b/tutorial/04-wrench.text
new file mode 100644
--- /dev/null
+++ b/tutorial/04-wrench.text
@@ -0,0 +1,1 @@
+the wrench moves in a straight line until blocked
diff --git a/tutorial/05-blockWrench.lock b/tutorial/05-blockWrench.lock
deleted file mode 100644
--- a/tutorial/05-blockWrench.lock
+++ /dev/null
@@ -1,11 +0,0 @@
-       " " " " " "       
-      " #         " " "  
-     " #   #   %   "   " 
-    " #   # S S % % % % "
-   "                 " " 
-  "   # #   #   # #   "  
- " "   #   # #   #   "   
-" * '               "    
- " @ "   # # # #   "     
-  " " "           "      
-       " " " " " "       
diff --git a/tutorial/05-blockWrench.text b/tutorial/05-blockWrench.text
deleted file mode 100644
--- a/tutorial/05-blockWrench.text
+++ /dev/null
@@ -1,1 +0,0 @@
-the hook can be used to guide the wrench
diff --git a/tutorial/05-hook.lock b/tutorial/05-hook.lock
new file mode 100644
--- /dev/null
+++ b/tutorial/05-hook.lock
@@ -0,0 +1,13 @@
+       " " " " " " "       
+      " %   # #   % " " "  
+     " %     Z #   % " # " 
+    " % %   % Z     % #   "
+   " %   # % % Z     # " " 
+  " % %   Z % % Z     # "  
+ " % % %   Z % % %     # " 
+  " % % %   % % %   % # "  
+ " "   % ]     %   # # "   
+" * '     ]   % %   # "    
+ " @ "   # #   % % 7 "     
+  " " "   % % % % 7 "      
+       " " " " " " "       
diff --git a/tutorial/05-hook.text b/tutorial/05-hook.text
new file mode 100644
--- /dev/null
+++ b/tutorial/05-hook.text
@@ -0,0 +1,1 @@
+turn hook with mouse wheel (or keys) to pull springs aside
diff --git a/tutorial/06-balls.lock b/tutorial/06-balls.lock
deleted file mode 100644
--- a/tutorial/06-balls.lock
+++ /dev/null
@@ -1,11 +0,0 @@
-       " " " " " "       
-      " & & S S # " " "  
-     " & & &     # "   " 
-    " & & & &   # # # # "
-   "         &   #   " " 
-  "     #   & &   #   "  
- " " O #     &       "   
-" * ' # #     O #   "    
- " @ " #   O     # "     
-  " " " #       # "      
-       " " " " " "       
diff --git a/tutorial/06-balls.text b/tutorial/06-balls.text
deleted file mode 100644
--- a/tutorial/06-balls.text
+++ /dev/null
@@ -1,1 +0,0 @@
-balls can be pushed around to allow or block movement
diff --git a/tutorial/06-blockWrench.lock b/tutorial/06-blockWrench.lock
new file mode 100644
--- /dev/null
+++ b/tutorial/06-blockWrench.lock
@@ -0,0 +1,11 @@
+       " " " " " "       
+      " #         " " "  
+     " #   #   %   "   " 
+    " #   # S S % % % % "
+   "                 " " 
+  "   # #   #   # #   "  
+ " "   #   # #   #   "   
+" * '               "    
+ " @ "   # # # #   "     
+  " " "           "      
+       " " " " " "       
diff --git a/tutorial/06-blockWrench.text b/tutorial/06-blockWrench.text
new file mode 100644
--- /dev/null
+++ b/tutorial/06-blockWrench.text
@@ -0,0 +1,1 @@
+the hook can be used to guide the wrench
diff --git a/tutorial/07-balls.lock b/tutorial/07-balls.lock
new file mode 100644
--- /dev/null
+++ b/tutorial/07-balls.lock
@@ -0,0 +1,11 @@
+       " " " " " "       
+      " & & S S # " " "  
+     " & & &     # "   " 
+    " & & & &   # # # # "
+   "         &   #   " " 
+  "     #   & &   #   "  
+ " " O #     &       "   
+" * ' # #     O #   "    
+ " @ " #   O     # "     
+  " " " #       # "      
+       " " " " " "       
diff --git a/tutorial/07-balls.text b/tutorial/07-balls.text
new file mode 100644
--- /dev/null
+++ b/tutorial/07-balls.text
@@ -0,0 +1,1 @@
+balls can be pushed around to allow or block movement
diff --git a/tutorial/07-traps.lock b/tutorial/07-traps.lock
deleted file mode 100644
--- a/tutorial/07-traps.lock
+++ /dev/null
@@ -1,13 +0,0 @@
-       & & & & & & &       
-      &             & & &  
-     & % O % S S "   &   & 
-    &         #   " " " " &
-   & "         O   " " & & 
-  & " "   # #     # # # &  
- & "                   # & 
-  & " " "         # # # &  
- & &   " } # #   #     &   
-& * '     % # #   #   &    
- & @ &     O       # &     
-  & & &     # # # # &      
-       & & & & & & &       
diff --git a/tutorial/07-traps.text b/tutorial/07-traps.text
deleted file mode 100644
--- a/tutorial/07-traps.text
+++ /dev/null
@@ -1,1 +0,0 @@
-if you make a mistake, you can undo moves ('X') or reset to the start ('^')
diff --git a/tutorial/08-springs.lock b/tutorial/08-springs.lock
deleted file mode 100644
--- a/tutorial/08-springs.lock
+++ /dev/null
@@ -1,17 +0,0 @@
-        " " " " " " " " "          
-       " # # # # # # # # " " " "   
-      " # #               "     "  
-     " # #   % % % %   #   " # # " 
-    " # # #   % % %     # # #     "
-   "               %     # # "   " 
-  "   # #   # # #   %     # # " "  
- "     ]   " S S #   %       # "   
-"       ]   " % % % % % %     # "  
- "       ]   " ] % % % % %   7 "   
-  "       ] % " ] % % % %   7 "    
- " "     % % % " " % % S S & "     
-" * '     % Z   o - % % % % "      
- " @ "       Z / & C C C C "       
-  " " "       Z & & & &   "        
-       "       Z & & &   "         
-        " " " " " " " " "          
diff --git a/tutorial/08-springs.text b/tutorial/08-springs.text
deleted file mode 100644
--- a/tutorial/08-springs.text
+++ /dev/null
@@ -1,1 +0,0 @@
-A spring's length can double when stretched, and halve when compressed
diff --git a/tutorial/08-traps.lock b/tutorial/08-traps.lock
new file mode 100644
--- /dev/null
+++ b/tutorial/08-traps.lock
@@ -0,0 +1,13 @@
+       & & & & & & &       
+      &             & & &  
+     & % O % S S "   &   & 
+    &         #   " " " " &
+   & "         O   " " & & 
+  & " "   # #     # # # &  
+ & "                   # & 
+  & " " "         # # # &  
+ & &   " } # #   #     &   
+& * '     % # #   #   &    
+ & @ &     O       # &     
+  & & &     # # # # &      
+       & & & & & & &       
diff --git a/tutorial/08-traps.text b/tutorial/08-traps.text
new file mode 100644
--- /dev/null
+++ b/tutorial/08-traps.text
@@ -0,0 +1,1 @@
+if you make a mistake, you can undo ('X') or reset ('^')
diff --git a/tutorial/09-pivots.lock b/tutorial/09-pivots.lock
deleted file mode 100644
--- a/tutorial/09-pivots.lock
+++ /dev/null
@@ -1,11 +0,0 @@
-       " " " " " "       
-      " \     # # " " "  
-     "   o -   \ ' " # " 
-    " % /   "   o - #   "
-   " % %   " "   # # " " 
-  " % % %   " O   # # "  
- " "         . o - 7 "   
-" * ' . o - &   ` 7 "    
- " @ " O     & & & "     
-  " " " # #   & & "      
-       " " " " " "       
diff --git a/tutorial/09-pivots.text b/tutorial/09-pivots.text
deleted file mode 100644
--- a/tutorial/09-pivots.text
+++ /dev/null
@@ -1,1 +0,0 @@
-push arms to rotate them around their pivots
diff --git a/tutorial/09-springs.lock b/tutorial/09-springs.lock
new file mode 100644
--- /dev/null
+++ b/tutorial/09-springs.lock
@@ -0,0 +1,17 @@
+        " " " " " " " " "          
+       " # # # # # # # # " " " "   
+      " # #               "     "  
+     " # #   % % % %   #   " # # " 
+    " # # #   % % %     # # #     "
+   "               %     # # "   " 
+  "   # #   # # #   %     # # " "  
+ "     ]   " S S #   %       # "   
+"       ]   " % % % % % %     # "  
+ "       ]   " ] % % % % %   7 "   
+  "       ] % " ] % % % %   7 "    
+ " "     % % % " " % % S S & "     
+" * '     % Z   o - % % % % "      
+ " @ "       Z / & C C C C "       
+  " " "       Z & & & &   "        
+       "       Z & & &   "         
+        " " " " " " " " "          
diff --git a/tutorial/09-springs.text b/tutorial/09-springs.text
new file mode 100644
--- /dev/null
+++ b/tutorial/09-springs.text
@@ -0,0 +1,1 @@
+A spring's length can double when stretched, and halve when compressed
diff --git a/tutorial/10-pivots.lock b/tutorial/10-pivots.lock
new file mode 100644
--- /dev/null
+++ b/tutorial/10-pivots.lock
@@ -0,0 +1,11 @@
+       " " " " " "       
+      " \     # # " " "  
+     "   o -   \ ' " # " 
+    " % /   "   o - #   "
+   " % %   " "   # # " " 
+  " % % %   " O   # # "  
+ " "         . o - 7 "   
+" * ' . o - &   ` 7 "    
+ " @ " O     & & & "     
+  " " " # #   & & "      
+       " " " " " "       
diff --git a/tutorial/10-pivots.text b/tutorial/10-pivots.text
new file mode 100644
--- /dev/null
+++ b/tutorial/10-pivots.text
@@ -0,0 +1,1 @@
+push arms to rotate them around their pivots
