diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,23 @@
+## [v0.11.0.1](https://github.com/LambdaHack/LambdaHack/compare/v0.11.0.0...v0.11.0.1)
+
+- Split the long Content.ModeKind texts into lines, [#283](https://github.com/LambdaHack/LambdaHack/issues/283)
+- In unit tests, re-use values from UnitTestHelpers and Game.LambdaHack.Common.Kind, [#286](https://github.com/LambdaHack/LambdaHack/issues/286)
+- Rename unsnoc (see #300)
+- Bump the bound on hsini to handle trailing whitespace properly
+- Hack around a crash when TABbing during item manipulation
+- Document better that macros start with Cmd_
+- Correct a typo in test/Spec.hs
+- Add the communication overhead explanation proposed by fYnch
+- Compress detail levels and don't show paths at DetailHigh
+- Balance speedrunning and explain it better in player docs
+- Change failed alter message to less confusing; thanks go to Pug
+- Tweak tutorial hint enums and tests
+- Make lore menu with all tutorial hints
+- Don't punish naughty window managers with spammy warning
+- Try harder to prevent resizing in SDL
+- Change install from Hackage instructions so that they work outside package dir
+- Interrupt sleep and running a bit less
+
 ## [v0.11.0.0](https://github.com/LambdaHack/LambdaHack/compare/v0.10.3.0...v0.11.0.0)
 
 - Partially work around regression in libsdl2 2.0.16 (https://github.com/LambdaHack/LambdaHack/issues/281); to also avoid deformed boxes around tiles on the game map, please switch to a different SDL2 version
diff --git a/COPYLEFT b/COPYLEFT
--- a/COPYLEFT
+++ b/COPYLEFT
@@ -5,19 +5,19 @@
 
 Files: *
 Copyright: 2008-2011 Andres Loeh
-           2010-2021 Mikolaj Konarski and others (see git history)
+           2010-2023 Mikolaj Konarski and others (see git history)
 License: BSD-3-Clause
 
 Files: GameDefinition/fonts/*.fnt GameDefinition/fonts/*.bdf GameDefinition/fonts/16x16xw.woff
 Copyright: 1997-2016 Leon Marrick
            1997-2016 Sheldon Simms III
            1997-2016 Nick McConnell
-           2016-2021 Mikolaj Konarski
+           2016-2023 Mikolaj Konarski
 License: GPL-2.0-or-later
 
 Files: GameDefinition/fonts/Binary*.woff
 Copyright 2010-2019 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'
-Copyright 2021 Mikolaj Konarski
+Copyright 2023 Mikolaj Konarski
 License: OFL-1.1
 
 Files: GameDefinition/fonts/DejaVu*.woff
diff --git a/CREDITS b/CREDITS
--- a/CREDITS
+++ b/CREDITS
@@ -13,13 +13,14 @@
 bulbousBullfrog
 Alex Byaly
 Jamie Fristrom
+Kayvan Kazeminejad
 
 
 
 Binary distributions of this package may be linked or bundled with libraries
 such as SDL2, SDL_ttf, FreeType and many others. These libraries
 are copyright of their respective owners, with all rights reserved.
-In particular, portions of this software are copyright © 2021 The FreeType
+In particular, portions of this software are copyright © 2023 The FreeType
 Project (www.freetype.org). All rights reserved.
 
 
diff --git a/GameDefinition/Content/ModeKind.hs b/GameDefinition/Content/ModeKind.hs
--- a/GameDefinition/Content/ModeKind.hs
+++ b/GameDefinition/Content/ModeKind.hs
@@ -125,7 +125,11 @@
   , mdesc   = "Your research team disagrees over a drink with some gentlemen scientists about premises of a relative completeness theorem and there's only one way to settle that."
       -- Not enough space with square fonts and also this is more of a hint than a flavour: Remember to keep your party together when opponents are spotted, or they might be tempted to silence solitary disputants one by one and so win the altercation.
   , mreason = "In addition to advancing game plot, this encounter trains melee, squad formation and stealth. The battle is symmetric in goals (incapacitate all enemies) and in squad capabilities (only the pointman moves, others either melee or wait)."
-  , mhint   = "Run a short distance with Shift or LMB, switch the pointman with Tab, repeat. In open terrain, if you keep distance between teammates, this resembles the leap frog infantry tactics. For best effects, end each sprint behind a cover or concealment.\nObserve and mimic the enemies. If you can't see an enemy that apparently can see you, in reversed circumstances you would have the same advantage. Savour the relative fairness --- you won't find any in the main crawl adventure that follows.\nIf you get beaten repeatedly, try using all consumables you find. Ponder the hints from the defeat message, in particular the one about keeping your party together once the opponents are spotted. However, if you want to discover a winning tactics on your own, make sure to ignore any such tips until you succeed."
+  , mhint   =  T.intercalate "\n"
+    [ "Run a short distance with Shift or LMB, switch the pointman with Tab, repeat. In open terrain, if you keep distance between teammates, this resembles the leap frog infantry tactics. For best effects, end each sprint behind a cover or concealment."
+    , "Observe and mimic the enemies. If you can't see an enemy that apparently can see you, in reversed circumstances you would have the same advantage. Savour the relative fairness --- you won't find any in the main crawl adventure that follows."
+    , "If you get beaten repeatedly, try using all consumables you find. Ponder the hints from the defeat message, in particular the one about keeping your party together once the opponents are spotted. However, if you want to discover a winning tactics on your own, make sure to ignore any such tips until you succeed."
+    ]
   }
 
 crawl = ModeKind
@@ -135,7 +139,7 @@
   , mattract = False
   , mroster = rosterCrawl
   , mcaves  = cavesCrawl
-  , mendMsg = [ (Killed, "To think that followers of science and agents of enlightenment would earn death as their reward! Where did we err in our ways? Perhaps nature should not have been disturbed so brashly and the fell beasts woken up from their slumber so eagerly?\nPerhaps the gathered items should have been used for scientific experiments on the spot rather than hoarded as if of base covetousness? Or perhaps the challenge, chosen freely but without the foreknowledge of the grisly difficulty, was insurmountable and forlorn from the start, despite the enormous power of educated reason at out disposal?")
+  , mendMsg = [ (Killed, killedMsg)
               , (Escape, "It's better to live to tell the tale than to choke on more than one can swallow. There was no more exquisite cultural artifacts and glorious scientific wonders in these forbidding tunnels anyway. Or were there?") ]
   , mrules  = T.intercalate "\n"
       [ "* Many levels"
@@ -144,9 +148,14 @@
       , "* Find a way out and escape ASAP"
       ]
   , mdesc   = "Enjoy the peaceful seclusion of these cold austere tunnels, but don't let wanton curiosity, greed and the ever-creeping abstraction madness keep you down there for too long. If you find survivors (whole or perturbed or segmented) of the past scientific missions, exercise extreme caution and engage or ignore at your discretion."
-  , mreason = "This is the main, longest and most replayable scenario of the game."
+  , mreason = "This is the main, longest and most replayable scenario of the game. It's crucial that you gather the most interesting cultural artifacts such as gold, gems and elixirs. Equally importantly, you have to limit the permanent sanity deterioration of your scientific expedition members by minimizing the time they are exposed to the horrors of the underworld."
   , mhint   = "If you keep dying, attempt the subsequent adventures as a breather (perhaps at lowered difficulty). They fill the gaps in the plot and teach particular skills that may come in handy and help you discover new tactics of your own or come up with a strategy for staving off the attrition. Also experimenting with the initial adventures may answer some questions you didn't have when you attempted them originally."
   }
+ where
+   killedMsg = T.intercalate "\n"
+     [ "To think that followers of science and agents of enlightenment would earn death as their reward! Where did we err in our ways? Perhaps nature should not have been disturbed so brashly and the fell beasts woken up from their slumber so eagerly?"
+     , "Perhaps the gathered items should have been used for scientific experiments on the spot rather than hoarded as if of base covetousness? Or perhaps the challenge, chosen freely but without the foreknowledge of the grisly difficulty, was insurmountable and forlorn from the start, despite the enormous power of educated reason at out disposal?"
+     ]
 
 -- The trajectory tip is important because of tactics of scout looking from
 -- behind a bush and others hiding in mist. If no suitable bushes,
@@ -163,7 +172,7 @@
   , mattract = False
   , mroster = rosterShootout
   , mcaves  = cavesShootout
-  , mendMsg = [ (Killed, "This is a disgrace. What have we missed in our theoretic models of this fight? Did we miss a human lookout placed in a covered but unobstructed spot that lets the rest of the squad snipe from concealment or from a safe distance?\nBarring that, would we end up in a better shape even if we all hid and only fired blindly? We'd listen to impact sounds and wait vigilantly for incoming enemy missiles in order to register their trajectories and derive hints of enemy location. Apparently, ranged combat requires a change of pace and better planning than our previous simple but efficient calculations accustomed us to.")
+  , mendMsg = [ (Killed, killedMsg)
               , (Conquer, "That was a good fight, with scientifically accurate application of missiles, cover and concealment. Not even skilled logicians can routinely deduce enemy position from the physical trajectory of their projectiles nor by firing without line of sight and interpreting auditory cues. However, while this steep hurdle is overcome, the dispute is not over yet.") ]
   , mrules  = T.intercalate "\n"
       [ "* One level only"
@@ -175,6 +184,11 @@
   , mreason = "This adventure teaches the ranged combat skill in the simplified setup of fully symmetric battle."
   , mhint   = "Try to come up with the best squad formation for this tactical challenge. Don't despair if you run out of ammo, because if you aim truly, enemy has few hit points left at this point. In turn, when trying to avoid enemy projectiles, you can display the trajectory of any soaring entity by pointing it with the crosshair in aiming mode."
   }
+ where
+   killedMsg = T.intercalate "\n"
+     [ "This is a disgrace. What have we missed in our theoretic models of this fight? Did we miss a human lookout placed in a covered but unobstructed spot that lets the rest of the squad snipe from concealment or from a safe distance?"
+     , "Barring that, would we end up in a better shape even if we all hid and only fired blindly? We'd listen to impact sounds and wait vigilantly for incoming enemy missiles in order to register their trajectories and derive hints of enemy location. Apparently, ranged combat requires a change of pace and better planning than our previous simple but efficient calculations accustomed us to."
+     ]
 
 hunt = ModeKind  -- melee vs ranged with reaction fire in daylight
   { mname   = "perilous hunt (4)"
@@ -204,7 +218,7 @@
   , mattract = False
   , mroster = rosterFlight
   , mcaves  = cavesFlight
-  , mendMsg = [ (Killed, "Somebody must have tipped the enemies of free inquiry off. However, us walking along a lit trail, yelling, could have been a contributing factor. Also, it's worth noting that the torches prepared for this assault are best used as thrown makeshift flares.\nOn the other hand, equipping a lit torch makes one visible in the dark, regrettably but not quite unexpectedly to a scientific mind. Lastly, the goal of this foray was to definitely disengage from the fruitless dispute, via a way out marked by a yellow '>' sign, and to gather treasure that would support our future research. Not to harass every nearby scientific truth denier, as much as they do deserve it.")
+  , mendMsg = [ (Killed, killedMsg)
               , (Conquer, "It was enough to reach the escape area marked by yellow '>' symbol. Spilling that much blood was risky. unnecessary and alerted the authorities. Having said that --- impressive indeed.") ]
   , mrules  = T.intercalate "\n"
       [ "* One level only"
@@ -217,6 +231,11 @@
   , mreason = "The focus of this installment is on stealthy exploration under the threat of numerically superior enemy."
   , mhint   = ""
   }
+ where
+   killedMsg = T.intercalate "\n"
+     [ "Somebody must have tipped the enemies of free inquiry off. However, us walking along a lit trail, yelling, could have been a contributing factor. Also, it's worth noting that the torches prepared for this assault are best used as thrown makeshift flares."
+     , "On the other hand, equipping a lit torch makes one visible in the dark, regrettably but not quite unexpectedly to a scientific mind. Lastly, the goal of this foray was to definitely disengage from the fruitless dispute, via a way out marked by a yellow '>' sign, and to gather treasure that would support our future research. Not to harass every nearby scientific truth denier, as much as they do deserve it."
+     ]
 
 zoo = ModeKind  -- asymmetric crowd melee at night
   { mname   = "burning zoo (6)"
@@ -253,7 +272,7 @@
   , mattract = False
   , mroster = rosterAmbush
   , mcaves  = cavesAmbush
-  , mendMsg = [ (Killed, "You turned out to be the prey, this time, not the hunter. In fact, you are not even in the hunters' league. When fighting against such odds, passively waiting for enemy to spring a trap is to no avail, because a professional team can sneak in darkness and ambush the ambushers.\nGranted, good positioning is crucial, so that each squad member can overwatch the battlefield and fire opportunistically, using the recently recovered instant telegraphy equipment. However, there is no hope without active scouting, throwing lit objects and probing suspect areas with missiles while paying attention to sounds. And that may still not be enough.")
+  , mendMsg = [ (Killed, killedMsg)
               , (Conquer, "The new instant telegraphy equipment enabling simultaneous ranged attacks with indirect triangulation and aiming proved effective beyond expectation. Your ideas are safe, your research programme on track, your chartered company ready to launch and introduce progress and science into every household of the nation.") ]
   , mrules  = T.intercalate "\n"
       [ "* One level only"
@@ -265,6 +284,11 @@
   , mreason = "In this adventure, finally, your heroes are able to all use ranged attacks at once, given enough ammunition."
   , mhint   = "Beware of friendly fire, particularly from explosives. But you need no more hints. Go fulfill your destiny! For Science!"
   }
+ where
+   killedMsg = T.intercalate "\n"
+     [ "You turned out to be the prey, this time, not the hunter. In fact, you are not even in the hunters' league. When fighting against such odds, passively waiting for enemy to spring a trap is to no avail, because a professional team can sneak in darkness and ambush the ambushers."
+     , "Granted, good positioning is crucial, so that each squad member can overwatch the battlefield and fire opportunistically, using the recently recovered instant telegraphy equipment. However, there is no hope without active scouting, throwing lit objects and probing suspect areas with missiles while paying attention to sounds. And that may still not be enough."
+     ]
 
 safari = ModeKind  -- Easter egg available only via screensaver
   { mname   = "safari"
diff --git a/GameDefinition/PLAYING.md b/GameDefinition/PLAYING.md
--- a/GameDefinition/PLAYING.md
+++ b/GameDefinition/PLAYING.md
@@ -410,9 +410,9 @@
 This time, the *ranged* armor skill of the defender is taken into account
 and, additionally, the speed of the missile (based on shape and weight)
 figures in the calculation. You may propel any item from your inventory
-(by default you are offered only the appropriate items; press `+` to open
-all choices). Only items of a few kinds inflict direct damage, but some
-have other effects, beneficial, detrimental or mixed.
+(by default you are offered exclusively the appropriate items; press `+`
+to open all choices). Only items of a few kinds inflict direct damage,
+but some have other effects, beneficial, detrimental or mixed.
 
 In-game detailed item descriptions contain melee and ranged damage estimates.
 They do not take into account enemy armor nor damage from effects and,
@@ -420,19 +420,20 @@
 in question. The displayed figures are rounded, but the game internally
 keeps track of minute fractions of HP for all actors in all calculations.
 
-The combat stress drains Calm, gradually limiting viewing radius and,
-if Calm reaches zero and the actor is sufficiently impressed by his foes,
-making him defect and surrender unto their domination. Whenever the monster's
-or hero's hit points reach zero, the combatant falls down and quickly
-gets permanently incapacitated. When the last hero is disabled or dominated,
-the adventure ends in defeat.
+The combat stress drains Calm, gradually limiting viewing radius.
+If Calm reaches zero and the actor is sufficiently impressed by his foes,
+the actor defects and surrenders unto their domination. Whenever
+the monster's or hero's hit points reach zero, the combatant falls down
+and gets permanently incapacitated. When the last hero is disabled
+or dominated, the adventure ends in defeat. Eliminating the last
+monster at a given time not always results in victory.
 
 
 Attacking from a distance
 -------------------------
 
-Before the player presses `f` to make a ranged attack, he may move
-and set the aiming crosshair in aiming mode. However, this is
+Before the player presses `f` to make a ranged attack, he may spend time
+moving and setting the aiming crosshair in aiming mode. However, this is
 not often needed, since crosshair is set automatically as soon
 as a monster comes into view and can still be adjusted for as long
 as the missile to fling is not chosen.
@@ -458,25 +459,72 @@
 known position.
 
 
+Combat and Speed
+----------------
+
+During combat there are some more nuances to movement and actions that
+are worth considering. Every action takes an amount of time (equal for
+most of the actions). The time that it takes a unit to act is relative
+to its speed and additionally affected by the team's communication overhead.
+
+Any member of the squad that has not idled (either moved, used inventory,
+attacked or lurked attentively) will incur a 10% speed penalty
+on all members of the team on the same level, including themselves,
+but excluding the pointman. This is a penalty for the time
+and energy they had to spend to take note of any changes
+that could be relevant to the squad and for the overhead needed
+to communicate it and get it acknowledged by the other squad members.
+In this way, a squad where 4 teammates take actions every turn will work
+at 60% speed. By the same token, regardless of how numerous the team is,
+never more than 10 normal speed teammates can act in the same turn
+(the moves of the others are going to spill over to the next turn).
+
+The pointman is an exception to this rule. The pointman may perform
+melee attacks or any other action without incurring a team overhead penalty.
+This may make it preferable to melee with the pointman, even though
+any team member can melee and pointmen can also perform many other actions.
+Also, this prevents a pointman from being slowed by the team residing
+elsewhere on the level, making any solo pointman vs pointman duels fairer.
+
+In a short scenario this may not be terribly relevant, but in longer
+engagements the overhead can make a big difference in damage per second
+that a team can put out. E.g., it's a crucial and context-dependent choice
+whether to hurry towards a lonely meleeing teammate, thus lowering
+the damage output, stay put, fling a projectile, or perhaps get the meleeing
+teammate to retreat towards the team. Over a long run, such choices
+influence the speed of HP attrition and if it outweighs the benefits
+of exploration.
+
+
 Winning and dying
 -----------------
 
-You win an adventure if you escape the location alive (which may prove
-difficult, because your foes tend to gradually build up the ambush squad
-blocking your escape route) or, in scenarios with no escape locations,
-if you eliminate all opposition. In the former case, your score
-is based predominantly on the gold and precious gems you've plundered.
-In the latter case, your score is most influenced by the number
+You win an adventure if you escape the situation alive, which may prove
+difficult, because your foes tend to gradually build up an ambush squad
+blocking your escape route. On the other hand, in scenarios with no open
+escape routes, the only way to survive is to eliminate all opposition,
+which is not a small feat, either.
+
+If escape without a fight to the death is possible, your score is based
+in equal parts on the speed with which you manage to leave
+the deadly environment and the proportion of gold and precious gems
+(including the elixirs that are stored in precious crystal boxes)
+you manage to plunder and carry off. Note that engaging in wanton slaughter
+is counterproductive in such a setting, because it wastes time
+and non-unique enemies never carry valuables, but stash them instead.
+
+If killing all foes is required, your score is most influenced by the number
 of turns you spent overcoming your foes (the quicker the victory, the better;
 the slower the demise, the better). Bonus points, affected by the number
-of heroes lost, are awarded only if you win. The score is heavily
-modified by the chosen game difficulty, but not by any other challenges
-(which are, however, proudly displayed in the high score listing).
+of heroes lost, are awarded only if you win. The score, in all cases, is
+heavily modified by the chosen game difficulty, but not by any other challenges
+(which are, however, distinctly announced in the high score listing).
 
-When all your heroes fall, you are going to invariably see a new foolhardy
-party of adventurers clamoring to be led into the unknown perils.
-They start their conquest afresh, with no experience, no supplies
-for survival and no equipment, and new undaunted enemies bar their way.
+When all your heroes fall, you lose, but you are going to invariably
+see a new foolhardy party of adventurers clamoring to be led
+into the unknown perils. They start their conquest afresh,
+with no experience, no supplies for survival and no equipment,
+and new undaunted enemies bar their way.
 Lead the new hopeful explorers with wisdom and fortitude!
 
 
diff --git a/GameDefinition/config.ui.default b/GameDefinition/config.ui.default
--- a/GameDefinition/config.ui.default
+++ b/GameDefinition/config.ui.default
@@ -17,13 +17,15 @@
 Cmd_2 = ("KP_Insert", ([CmdAim], "", ByAimMode AimModeCmd {exploration = Dashboard, aiming = Accept}))
 ; Custom key rebinding example:
 ;
-;   Cmd_3 = ("a", ([CmdItem], "My Happy Cmd", Macro ["t"]))
+;   Cmd_anyName_youLike = ("a", ([CmdItem], "My Happy Cmd", Macro ["t"]))
 ;
 ; Explanation: to (t)rigger a consumable item for use, you normally use
 ;              the key press 't'.
 ; Suppose you would like to use the key press 'a' instead of 't'.
-;   Cmd_3          - can be anything as long as it doesn't conflict or
-;                    overlap with a binding already defined
+;   Cmd_anyName_youLike
+;                  - can be anything starting with 'Cmd_' (the "Cmd_" prefix
+;                    specifies that you are assigning a new command), as long
+;                    as it doesn't conflict with a binding already defined
 ;   "a"            - the new key to use
 ;   [CmdItem]      - can either be [] or [CmdItem]. If using [CmdItem]
 ;                    the new keybinding will be displayed in the help menu
@@ -32,6 +34,17 @@
 ;                    in the help menu, it is recommended to leave this empty []
 ;   "My Happy Cmd" - Is a name given to your binding, which would be dispayed
 ;                    in the help page in-game if you use 'CmdItem' from above
+;   Macro ["t"]    - it specifies that you create a new command by combining
+;                    or using the existing ones bound to keys. You can combine
+;                    several key names within these squared brackets.
+;                    Each key name must be enveloped in quotation marks.
+;                    If you want to create a command that combines several
+;                    simple actions, you should create a sequence of keys
+;                    separated by the "," mark.
+;                    For example, you may write something like ["r", "*"],
+;                    which will remove all items from the shared inventory
+;                    directly to the floor. Using Macro commands is the easiest
+;                    way to create custom commands.
 
 [hero_names]
 HeroName_0 = ("Haskell Alvin", "he")
diff --git a/LambdaHack.cabal b/LambdaHack.cabal
--- a/LambdaHack.cabal
+++ b/LambdaHack.cabal
@@ -6,7 +6,7 @@
 -- PVP summary:+-+------- breaking API changes
 --             | |  +----- minor or non-breaking API additions
 --             | |  | +--- code changes with no API change
-version:       0.11.0.0
+version:       0.11.0.1
 synopsis:      A game engine library for tactical squad ASCII roguelike dungeon crawlers
 description: LambdaHack is a Haskell game engine library for ASCII roguelike
              games of arbitrary theme, size and complexity, with optional
@@ -44,8 +44,7 @@
 bug-reports:   http://github.com/LambdaHack/LambdaHack/issues
 license:       BSD-3-Clause
 license-file:  COPYLEFT
-tested-with:   GHC==8.2.2, GHC==8.4.4, GHC==8.6.5, GHC==8.8.4, GHC==8.10.7,
-               GHC==9.0.1
+tested-with:   GHC==8.10.*, GHC==9.0.*, GHC==9.2.*, GHC==9.4.*
 -- Enough files is included in the Hackage package to run CI and manual
 -- tests off it, to be presentable on Hackage and to create user-friendly
 -- binary packages.
@@ -84,11 +83,6 @@
   default:            False
   manual:             True
 
-flag with_costly_optimizations
-  description:        turn on costly (mostly GHC heap size) optimizations (that give 15-25% speedup)
-  default:            True
-  manual:             True
-
 flag release
   description:        prepare for a release (expose internal functions and types, etc.)
   default:            True
@@ -121,9 +115,10 @@
 -- TODO: remove -Winvalid-haddock when added to -Wall in a GHC I use for haddock
   ghc-options:        -fno-ignore-asserts
 
-  if flag(with_costly_optimizations)
-    ghc-options:        -fexpose-all-unfoldings -fspecialise-aggressively -fsimpl-tick-factor=200
+  ghc-options:        -fexpose-all-unfoldings -fspecialise-aggressively -fsimpl-tick-factor=200
 
+  ghc-options:        -Wmissed-specialisations
+
   if flag(with_expensive_assertions)
     cpp-options:      -DWITH_EXPENSIVE_ASSERTIONS
 
@@ -230,6 +225,7 @@
                       Game.LambdaHack.Client.UI.SessionUI
                       Game.LambdaHack.Client.UI.Slideshow
                       Game.LambdaHack.Client.UI.SlideshowM
+                      Game.LambdaHack.Client.UI.TutorialHints
                       Game.LambdaHack.Client.UI.UIOptions
                       Game.LambdaHack.Client.UI.UIOptionsParse
                       Game.LambdaHack.Client.UI.Watch
@@ -318,7 +314,7 @@
                       file-embed >= 0.0.11,
                       filepath   >= 1.2.0.1,
                       hashable   >= 1.1.2.5,
-                      hsini      >= 0.2,
+                      hsini      >= 0.2 && < 0.5.2 || >= 0.5.2.2,
                       witch      >= 0.3,
                       keys       >= 3,
                       miniutter  >= 0.5.0.0,
diff --git a/Makefile b/Makefile
--- a/Makefile
+++ b/Makefile
@@ -32,10 +32,10 @@
 	google-chrome --no-sandbox --js-flags="--logfile=%t.log --prof" ../lambdahack.github.io/index.html
 
 minific:
-	npx google-closure-compiler dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.11.0.0/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js --compilation_level=ADVANCED_OPTIMIZATIONS --isolation_mode=IIFE --assume_function_wrapper --externs=dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.11.0.0/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js.externs --externs=/home/mikolaj/r/lambdahack.github.io/lz-string.extern.js --jscomp_off="*" > ../lambdahack.github.io/lambdahack.all.js
+	npx google-closure-compiler dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.11.0.1/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js --compilation_level=ADVANCED_OPTIMIZATIONS --isolation_mode=IIFE --assume_function_wrapper --externs=dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.11.0.1/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js.externs --externs=/home/mikolaj/r/lambdahack.github.io/lz-string.extern.js --jscomp_off="*" > ../lambdahack.github.io/lambdahack.all.js
 
 minificForNode:
-	npx google-closure-compiler dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.11.0.0/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js --compilation_level=ADVANCED_OPTIMIZATIONS --isolation_mode=IIFE --assume_function_wrapper --externs=/home/mikolaj/r/lambdahack.github.io/lz-string.extern.js --externs=dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.11.0.0/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js.externs --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/assert.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/child_process.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/crypto.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/dns.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/events.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/globals.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/https.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/os.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/punycode.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/readline.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/stream.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/tls.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/url.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/vm.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/buffer.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/cluster.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/dgram.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/domain.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/fs.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/http.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/net.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/path.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/querystring.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/repl.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/string_decoder.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/tty.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/util.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/zlib.js --jscomp_off="*" > ../lambdahack.github.io/lambdahack.all.js
+	npx google-closure-compiler dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.11.0.1/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js --compilation_level=ADVANCED_OPTIMIZATIONS --isolation_mode=IIFE --assume_function_wrapper --externs=/home/mikolaj/r/lambdahack.github.io/lz-string.extern.js --externs=dist-newstyle/build/x86_64-linux/ghcjs-8.6.0.1/LambdaHack-0.11.0.1/x/LambdaHack/build/LambdaHack/LambdaHack.jsexe/all.js.externs --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/assert.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/child_process.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/crypto.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/dns.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/events.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/globals.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/https.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/os.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/punycode.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/readline.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/stream.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/tls.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/url.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/vm.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/buffer.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/cluster.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/dgram.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/domain.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/fs.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/http.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/net.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/path.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/querystring.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/repl.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/string_decoder.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/tty.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/util.js --externs=/home/mikolaj/r/closure-compiler/contrib/nodejs/zlib.js --jscomp_off="*" > ../lambdahack.github.io/lambdahack.all.js
 
 # Low delay to display animations swiftly and not bore the public too much.
 # Delay can't be lower than 2, because browsers sometimes treat delay 1
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -174,10 +174,11 @@
 which may already be a part of your OS distribution, but if it's too old
 (version 3.4 or later is required) you can download the whole current
 compilation suite as described at https://www.haskell.org/downloads/.
-Get the LambdaHack package from Hackage[3] as follows
+You can get and run the LambdaHack package from Hackage[3] as follows
 
     cabal update
-    cabal run LambdaHack
+    cabal install LambdaHack
+    ~/.cabal/bin/LambdaHack
 
 For a newer snapshot, clone the source code from github[5]
 and run `cabal run LambdaHack` from the main directory.
diff --git a/definition-src/Game/LambdaHack/Content/FactionKind.hs b/definition-src/Game/LambdaHack/Content/FactionKind.hs
--- a/definition-src/Game/LambdaHack/Content/FactionKind.hs
+++ b/definition-src/Game/LambdaHack/Content/FactionKind.hs
@@ -126,7 +126,7 @@
     , [minBound..maxBound] )
   , ( [(HiConst, 200), (HiLoss, -10)]  -- normally, always positive
     , victoryOutcomes )
-  , ( [(HiSprint, -500)]  -- speed matters, but only if fast enough
+  , ( [(HiSprint, -1000)]  -- speed matters, but only if fast enough
     , victoryOutcomes )
   , ( [(HiBlitz, -100)]  -- speed matters always
     , victoryOutcomes )
@@ -134,7 +134,7 @@
     , deafeatOutcomes )
   ]
 
--- Heroes in long crawls rejoice in loot.
+-- Heroes in long crawls rejoice in loot. But speedrunning just as valuable.
 hiHeroLong =
   [ ( [(HiLoot, 10000)]  -- multiplied by fraction of collected
     , [minBound..maxBound] )
diff --git a/engine-src/Game/LambdaHack/Client/UI.hs b/engine-src/Game/LambdaHack/Client/UI.hs
--- a/engine-src/Game/LambdaHack/Client/UI.hs
+++ b/engine-src/Game/LambdaHack/Client/UI.hs
@@ -157,7 +157,7 @@
   report <- getsSession $ newReport . shistory
   modifySession $ \sess -> sess {sreportNull = nullVisibleReport report}
   slides <- reportToSlideshowKeepHalt False []
-  ovs <- case unsnoc slides of
+  ovs <- case unsnocSlideshow slides of
     Nothing -> return EM.empty
     Just (allButLast, (ov, _)) ->
       if allButLast == emptySlideshow
diff --git a/engine-src/Game/LambdaHack/Client/UI/DrawM.hs b/engine-src/Game/LambdaHack/Client/UI/DrawM.hs
--- a/engine-src/Game/LambdaHack/Client/UI/DrawM.hs
+++ b/engine-src/Game/LambdaHack/Client/UI/DrawM.hs
@@ -232,7 +232,9 @@
  sreportNull <- getsSession sreportNull
  let frameForallId = FrameForall $ const $ return ()
  case saimMode of
-   Just AimMode{detailLevel} | not sreportNull -> do
+   Just AimMode{detailLevel} | not sreportNull
+                               && detailLevel /= DetailHigh
+                               && detailLevel /= DetailLow -> do
      COps{corule=RuleContent{rWidthMax, rHeightMax}, coTileSpeedup}
        <- getsState scops
      StateClient{seps} <- getClient
@@ -576,7 +578,7 @@
   return
 #ifdef WITH_EXPENSIVE_ASSERTIONS
     $ assert (length status == 2 * _rWidthMax
-              `blame` map Color.charFromW32 status)
+              `blame` attrStringToString status)
 #endif
         status
 
@@ -777,7 +779,7 @@
               [] -> []  -- no timeout-free organ, e.g., rattlesnake or hornet
               (ldmg, lextra) : rest -> (ldmg ++ lbonus, lextra) : rest
             displayDmgAndExtra (ldmg, lextra) =
-              if map Color.charFromW32 ldmg == "0"
+              if attrStringToString ldmg == "0"
               then case lextra of
                 [] -> ldmg
                 _plus : lextraRest -> lextraRest
diff --git a/engine-src/Game/LambdaHack/Client/UI/EffectDescription.hs b/engine-src/Game/LambdaHack/Client/UI/EffectDescription.hs
--- a/engine-src/Game/LambdaHack/Client/UI/EffectDescription.hs
+++ b/engine-src/Game/LambdaHack/Client/UI/EffectDescription.hs
@@ -151,7 +151,7 @@
       in case ts of
         [] -> ""
         [wonder] -> wonder <+> sometimes
-        _ | detailLevel < DetailAll -> header
+        _ | detailLevel < DetailHigh -> header
         _ -> header <+> "[" <> T.intercalate ", " ts <> "]" <+> sometimes
     OneOf effs ->
       let ts = filter (/= "") $ map (effectToSuffix detailLevel) effs
@@ -161,7 +161,7 @@
       in case ts of
         [] -> ""
         [wonder] -> wonder <+> sometimes
-        _ | detailLevel < DetailAll -> header
+        _ | detailLevel < DetailHigh -> header
         _ -> header <+> "[" <> T.intercalate ", " ts <> "]" <+> sometimes
     OnSmash _ -> ""  -- printed inside a separate section
     OnCombine _ -> ""  -- printed inside a separate section
@@ -169,11 +169,11 @@
                   in if T.null t then "" else "(on user:" <+> t <> ")"
     NopEffect -> ""  -- never printed
     AndEffect (ConsumeItems tools raw) eff -> case detailLevel of
-      DetailAll ->
+     DetailLow -> "of crafting"
+     DetailMedium -> "of crafting (recipes in lore menu)"
+     _ ->
        let (tcraft, traw, ttools) = describeCrafting tools raw eff
        in tcraft <+> traw <+> ttools
-      DetailHigh -> "of crafting (recipes in lore menu)"
-      _ -> "of crafting"
     AndEffect eff1 eff2 ->
       let t = T.intercalate " and then "
               $ nub $ filter (not . T.null)
diff --git a/engine-src/Game/LambdaHack/Client/UI/Frontend/Sdl.hs b/engine-src/Game/LambdaHack/Client/UI/Frontend/Sdl.hs
--- a/engine-src/Game/LambdaHack/Client/UI/Frontend/Sdl.hs
+++ b/engine-src/Game/LambdaHack/Client/UI/Frontend/Sdl.hs
@@ -222,7 +222,10 @@
         , SDL.windowMode = case fromMaybe NotFullscreen sfullscreenMode of
             ModeChange -> SDL.Fullscreen
             BigBorderlessWindow -> SDL.FullscreenDesktop
-            NotFullscreen -> SDL.Windowed }
+            NotFullscreen -> SDL.Windowed
+        , SDL.windowResizable = False  -- the default, but just in case...
+        , SDL.windowHighDPI = True  -- possibly prevents resize for Retina
+        }
       rendererConfig = SDL.RendererConfig
         { rendererType          = if sbenchmark
                                   then SDL.AcceleratedRenderer
@@ -372,11 +375,13 @@
         SDL.QuitEvent -> forceShutdown sess
         SDL.WindowRestoredEvent{} -> redraw  -- e.g., unminimize
         SDL.WindowExposedEvent{} -> redraw  -- needed on Windows
-        SDL.WindowResizedEvent{} -> do
-          -- Eome window managers apparently are able to resize.
-          SDL.showSimpleMessageBox Nothing SDL.Warning
-            "Windows resize detected"
-            "Please resize the game and/or make it fullscreen via 'allFontsScale' and 'fullscreenMode' settings in the 'config.ui.ini' file. Resizing fonts via generic scaling algorithms gives poor results."
+        SDL.WindowResizedEvent{} ->
+          -- Some window managers apparently are able to resize.
+          -- And some send resize events at startup, even though
+          -- they don't resize eventually, so this is too much spam:
+          -- SDL.showSimpleMessageBox Nothing SDL.Warning
+          --  "Windows resize detected"
+          --  "Please resize the game and/or make it fullscreen via 'allFontsScale' and 'fullscreenMode' settings in the 'config.ui.ini' file. Resizing fonts via generic scaling algorithms gives poor results."
           redraw
         -- Probably not needed, because no textures nor their content lost:
         -- SDL.WindowShownEvent{} -> redraw
@@ -687,7 +692,7 @@
                                     , Color.HighlightNoneCursor ]) ()
             fg | even row && fgRaw == Color.White = Color.AltWhite
                | otherwise = fgRaw
-            t = T.pack $ map Color.charFromW32 $ w : sameRest
+            t = T.pack . attrStringToString  $ w : sameRest
         width <- drawPropChunk x row fg t
         drawPropLine (x + width) row otherRest
       drawPropChunk :: Int -> Int -> Color.Color -> T.Text -> IO Int
diff --git a/engine-src/Game/LambdaHack/Client/UI/HandleHelperM.hs b/engine-src/Game/LambdaHack/Client/UI/HandleHelperM.hs
--- a/engine-src/Game/LambdaHack/Client/UI/HandleHelperM.hs
+++ b/engine-src/Game/LambdaHack/Client/UI/HandleHelperM.hs
@@ -120,10 +120,14 @@
     _ | banned && lidV /= blid body ->
       failMsg $ showReqFailure NoChangeDunLeader
     [] -> failMsg "cannot pick any other pointman on this level"
-    (np, b, _) : _ -> do
-      success <- pickLeader verbose np
-      let !_A = assert (success `blame` "same leader"
-                                `swith` (leader, np, b)) ()
+    (np, _b, _) : _ -> do
+      _success <- pickLeader verbose np
+-- This apparently crashes rarely, probably becuase @leader@ gets out of sync
+-- with the leader in client state, during nested UI manipulations.
+-- The worst that can happen is TAB doing nothing and having to be repeated
+-- or perhaps item manipulation needs to be exited for TAB to work again.
+--      let !_A = assert (success `blame` "same leader"
+--                                `swith` (leader, np, b)) ()
       return Nothing
 
 -- | Switches current pointman to the previous in the whole dungeon, wrapping.
@@ -207,7 +211,7 @@
         (Just (iid, _, _), Just pos, Just leader) -> do
           b <- getsState $ getActorBody leader
           if lidV /= blid b  -- no range warnings on remote levels
-             || detailLevel aimMode < DetailAll  -- no spam
+             || detailLevel aimMode < DetailHigh  -- no spam
           then return []
           else do
             itemFull <- getsState $ itemToFull iid
@@ -278,7 +282,7 @@
               else viewItem itemFull
             phrase = makePhrase
               [partItemWsRanged rwidth side factionD displayRanged
-                                DetailMedium 4 k localTime itemFull kit]
+                                DetailLow 4 k localTime itemFull kit]
             ncha = ncharges localTime kit
             periodic = IA.checkFlag Ability.Periodic arItem
             !cLab = Color.AttrChar { acAttr = attrCursor
@@ -712,7 +716,7 @@
                   | isJust $ btrajectory body = "move through here"
                   | otherwise = resideVerb
           verbs = flyVerb : guardVerbs
-          projDesc | not (bproj body) || detail < DetailAll = ""
+          projDesc | not (bproj body) || detail < DetailHigh = ""
                    | otherwise =
             let kit = beqp body EM.! btrunk body
                 ps = [partItemMediumAW rwidth side factionD localTime
@@ -727,11 +731,11 @@
                   tfact = factionD EM.! tfid
               in "Originally of" <+> gname tfact
                  <> ", now fighting for" <+> dominatedBy <> "."
-            _ | detail < DetailAll -> ""  -- only domination worth spamming
+            _ | detail < DetailHigh -> ""  -- only domination worth spamming
             _ | bfid body == side -> ""  -- just one of us
             _ | bproj body -> "Launched by" <+> gname bfact <> "."
             _ -> "One of" <+> gname bfact <> "."
-          idesc = if detail < DetailAll
+          idesc = if detail < DetailHigh
                   then ""
                   else IK.idesc $ itemKind itemFull
           -- If many different actors, only list names.
@@ -823,7 +827,7 @@
       -- that looks stand over the items, because then he can check details
       -- with inventory commands (or look in aiming mode).
       detailExploration = if standingOn && Just side == (bfid <$> mb)
-                          then DetailMedium
+                          then DetailLow
                           else DetailAll
       detail = maybe detailExploration detailLevel saimMode
   localTime <- getsState $ getLocalTime lidV
@@ -850,9 +854,6 @@
         partItemWsDetail detail
                          rwidth side factionD k localTime (itemToF iid) kit
       (object, person) = case EM.assocs is of
-        [(_, (k, _))] | detail == DetailLow ->
-          (if k == 1 then "an item" else "an item stack", MU.Sg3rd)
-        _ | detail == DetailLow -> ("some items", MU.PlEtc)
         ii : _ : _ : _ | detail <= DetailMedium ->
           (MU.Phrase [nWs ii, "and other items"], MU.PlEtc)
         [ii@(_, (1, _))] -> (nWs ii, MU.Sg3rd)
@@ -951,7 +952,7 @@
                        else "The following items on the ground or in equipment enable special transformations:"
                             <+> tItems <> "."  -- not telling to what terrain
       modifyBlurb = alterBlurb <+> transformBlurb
-      midEOL = if detail < DetailHigh
+      midEOL = if detail < DetailMedium
                   || T.null stashBlurb && T.null actorsDesc
                   || T.null smellBlurb && T.null itemsBlurb
                   || null embedsList && T.null modifyBlurb
@@ -960,26 +961,18 @@
       ms = [ (MsgPromptAction, stashBlurb)
            , (actorMsgClass, actorsBlurb)
            , (MsgPromptGeneric, actorsDesc <> midEOL) ]
-           ++ [(MsgPromptGeneric, smellBlurb) | detail >= DetailHigh]
+           ++ [(MsgPromptGeneric, smellBlurb) | detail >= DetailMedium]
            ++ [(MsgPromptItems, itemsBlurb <> midEOL)]
-           ++ [(MsgPromptFocus, tileBlurb) | detail >= DetailHigh
-                                             || detail == DetailMedium
+           ++ [(MsgPromptFocus, tileBlurb) | detail >= DetailMedium
+                                             || detail == DetailLow
                                                 && not (null embedsList)]
-           ++ [(MsgPromptGeneric, placeBlurb) | detail >= DetailHigh]
+           ++ [(MsgPromptGeneric, placeBlurb) | detail >= DetailMedium]
            ++ case detail of
                 DetailLow -> []  -- not to obscure aiming line
-                DetailMedium ->
-                  [(MsgPromptMention, case embedsList of
-                    [] -> ""
-                    [(k, _)] ->
-                      ppEmbedName (1, if k == 1
-                                      then "an embedded item"
-                                      else "a stack of embedded items")
-                    _ -> ppEmbedName (9, "some embedded items"))]
                 _ -> let n = sum $ map fst embedsList
                          wWandW = MU.WWandW $ map snd embedsList
                      in [(MsgPromptMention, ppEmbedName (n, wWandW)) | n > 0]
-           ++ [(MsgPromptModify, modifyBlurb) | detail == DetailAll]
+           ++ [(MsgPromptModify, modifyBlurb) | detail >= DetailHigh]
   return $! if all (T.null . snd) ms && detail > DetailLow
             then [(MsgPromptFocus, tileBlurb)]
             else ms
diff --git a/engine-src/Game/LambdaHack/Client/UI/HandleHumanGlobalM.hs b/engine-src/Game/LambdaHack/Client/UI/HandleHumanGlobalM.hs
--- a/engine-src/Game/LambdaHack/Client/UI/HandleHumanGlobalM.hs
+++ b/engine-src/Game/LambdaHack/Client/UI/HandleHumanGlobalM.hs
@@ -518,7 +518,7 @@
                map (\(iid, kit) -> (getKind iid, (iid, kit))) (EM.assocs embeds)
              ilooks = map itemLook $ sortEmbeds cops t embedKindList
          failWith $ makePhrase $
-           ["there is no point kicking", MU.AW name]
+           ["there is no way to activate or modify", MU.AW name]
            ++ if EM.null embeds
               then []
               else ["with", MU.WWandW ilooks]
diff --git a/engine-src/Game/LambdaHack/Client/UI/HandleHumanLocalM.hs b/engine-src/Game/LambdaHack/Client/UI/HandleHumanLocalM.hs
--- a/engine-src/Game/LambdaHack/Client/UI/HandleHumanLocalM.hs
+++ b/engine-src/Game/LambdaHack/Client/UI/HandleHumanLocalM.hs
@@ -966,7 +966,7 @@
   doLook
 
 detailCycle :: DetailLevel -> DetailLevel
-detailCycle detail = if detail == maxBound then minBound else succ detail
+detailCycle detail = if detail == minBound then maxBound else pred detail
 
 -- * ClearTargetIfItemClear
 
diff --git a/engine-src/Game/LambdaHack/Client/UI/InventoryM.hs b/engine-src/Game/LambdaHack/Client/UI/InventoryM.hs
--- a/engine-src/Game/LambdaHack/Client/UI/InventoryM.hs
+++ b/engine-src/Game/LambdaHack/Client/UI/InventoryM.hs
@@ -200,7 +200,6 @@
                   Just (lid, _) ->
                     map MU.Text ["on level", tshow $ abs $ fromEnum lid]
                   Nothing -> [] )
-            _ -> ("see", [])
           ownObject = case cstore of
             CStash -> ["our", MU.Text t]
             _ -> [MU.WownW (MU.Text $ bpronoun bodyUI) $ MU.Text t]
diff --git a/engine-src/Game/LambdaHack/Client/UI/ItemDescription.hs b/engine-src/Game/LambdaHack/Client/UI/ItemDescription.hs
--- a/engine-src/Game/LambdaHack/Client/UI/ItemDescription.hs
+++ b/engine-src/Game/LambdaHack/Client/UI/ItemDescription.hs
@@ -138,7 +138,7 @@
         let ppA = kindAspectToSuffix
             ppE = effectToSuffix detLev
             reduce_a = maybe "?" tshow . Dice.reduceDice
-            restEs | detLev >= DetailHigh
+            restEs | detLev >= DetailMedium
                      || not (IA.checkFlag Ability.MinorEffects arItem) =
                      IK.ieffects itemKind
                    | otherwise = []
@@ -154,7 +154,7 @@
             onCombineRawTs = T.intercalate " " $ filter (not . T.null)
                              $ map (ppE . unCombine) combineEffsRaw
             onCombineRawTsTooLarge =
-              detailLevel >= DetailAll && T.length onCombineRawTs > 120
+              detailLevel >= DetailHigh && T.length onCombineRawTs > 120
             (combineEffs, noSmashCombineEffs) =
               if onCombineRawTsTooLarge
               then (combineEffsRaw, noSmashCombineEffsRaw)
@@ -227,7 +227,7 @@
           , [damageText]
             ++ [timeoutText | detLev > DetailLow && not periodic]
             ++ aes
-            ++ if detLev >= DetailAll
+            ++ if detLev >= DetailHigh
                then [onCombine, onSmash]
                else [onCombineRawTs] )
       hurtMult = armorHurtCalculation True (IA.aSkills arItem)
@@ -259,7 +259,7 @@
            , filter (/= "")
              $ elab
                : tsSplit
-               ++ if detailLevel >= DetailAll
+               ++ if detailLevel >= DetailHigh
                   then map kindAspectToSuffix aspectsAux
                   else [] )
   in (onCombineTsAss, aspectDescs, rangedDamageDesc)
@@ -268,7 +268,7 @@
 partItem :: Int -> FactionId -> FactionDict -> Time -> ItemFull -> ItemQuant
          -> (MU.Part, MU.Part)
 partItem width side factionD =
-  partItemN width side factionD False DetailMedium 4
+  partItemN width side factionD False DetailLow 4
 
 partItemShort :: Int -> FactionId -> FactionDict -> Time -> ItemFull
               -> ItemQuant
@@ -285,7 +285,7 @@
 partItemHigh :: Int -> FactionId -> FactionDict -> Time -> ItemFull -> ItemQuant
              -> ([Text], MU.Part, MU.Part)
 partItemHigh width side factionD =
-  partItemN3 width side factionD False DetailAll 100
+  partItemN3 width side factionD False DetailHigh 100
 
 -- The @count@ can be different than @itemK@ in @ItemFull@, e.g., when picking
 -- a subset of items to drop.
@@ -318,16 +318,16 @@
                  -> Int -> FactionId -> FactionDict -> Int -> Time -> ItemFull
                  -> ItemQuant
                  -> MU.Part
-partItemWsDetail DetailLow = \_ _ _ _ _ _ _ -> ""
-partItemWsDetail DetailMedium = partItemWsShortest
-partItemWsDetail DetailHigh = partItemWs
+partItemWsDetail DetailLow = partItemWsShortest
+partItemWsDetail DetailMedium = partItemWs
+partItemWsDetail DetailHigh = partItemWsLong
 partItemWsDetail DetailAll = partItemWsLong
 
 partItemWs :: Int -> FactionId -> FactionDict -> Int -> Time -> ItemFull
            -> ItemQuant
            -> MU.Part
 partItemWs width side factionD =
-  partItemWsRanged width side factionD False DetailMedium 4
+  partItemWsRanged width side factionD False DetailLow 4
 
 partItemWsShortest :: Int -> FactionId -> FactionDict -> Int -> Time -> ItemFull
                    -> ItemQuant
@@ -345,7 +345,7 @@
                -> ItemQuant
                -> MU.Part
 partItemWsLong width side factionD =
-  partItemWsRanged width side factionD False DetailHigh 100
+  partItemWsRanged width side factionD False DetailMedium 100
 
 partItemShortAW :: Int -> FactionId -> FactionDict -> Time -> ItemFull
                 -> ItemQuant
@@ -360,7 +360,7 @@
                  -> MU.Part
 partItemMediumAW width side factionD localTime itemFull kit =
   let (name, powers) =
-        partItemN width side factionD False DetailMedium 100 localTime
+        partItemN width side factionD False DetailLow 100 localTime
                   itemFull kit
       arItem = aspectRecordFull itemFull
       phrase = MU.Phrase [name, powers]
diff --git a/engine-src/Game/LambdaHack/Client/UI/MonadClientUI.hs b/engine-src/Game/LambdaHack/Client/UI/MonadClientUI.hs
--- a/engine-src/Game/LambdaHack/Client/UI/MonadClientUI.hs
+++ b/engine-src/Game/LambdaHack/Client/UI/MonadClientUI.hs
@@ -128,10 +128,9 @@
 -- | Push frames or delays to the frame queue. The frames depict
 -- the @lid@ level.
 displayFrames :: MonadClientUI m => LevelId -> PreFrames3 -> m ()
-displayFrames _ [] = return ()
+displayFrames _ [] = return ()  -- shortcut
 displayFrames lid frs = do
   let framesRaw = case frs of
-        [] -> []
         [Just ((bfr, ffr), (ovProp, ovSquare, ovMono))] ->
           [Just ( (FrameBase $ U.unsafeThaw bfr, ffr)
                 , (ovProp, ovSquare, ovMono) )]
diff --git a/engine-src/Game/LambdaHack/Client/UI/Msg.hs b/engine-src/Game/LambdaHack/Client/UI/Msg.hs
--- a/engine-src/Game/LambdaHack/Client/UI/Msg.hs
+++ b/engine-src/Game/LambdaHack/Client/UI/Msg.hs
@@ -241,6 +241,7 @@
     MsgHeardFaraway -> False
     -- MsgHeardNearby interrupts, even if running started while hearing close
     MsgRangedOthers -> False
+    MsgNeutralEvent -> False
     MsgAtFeetMinor -> False
     _ -> True
   MsgClassShow x -> case x of
@@ -447,7 +448,7 @@
      else as ++ stringToAS ("<x" ++ show n ++ ">")
 
 anyInReport :: (MsgClass -> Bool) -> Report -> Bool
-anyInReport f (Report xns) = any (f . msgClass. repMsg) xns
+anyInReport f (Report xns) = any (f . msgClass . repMsg) xns
 
 -- * History
 
diff --git a/engine-src/Game/LambdaHack/Client/UI/MsgM.hs b/engine-src/Game/LambdaHack/Client/UI/MsgM.hs
--- a/engine-src/Game/LambdaHack/Client/UI/MsgM.hs
+++ b/engine-src/Game/LambdaHack/Client/UI/MsgM.hs
@@ -1,7 +1,7 @@
 -- | Monadic operations on game messages.
 module Game.LambdaHack.Client.UI.MsgM
   ( msgAddDuplicate, msgAddDistinct, msgAdd, msgLnAdd
-  , promptMainKeys, recordHistory
+  , promptMainKeys, recordHistory, tutorialHintMsgAdd
   ) where
 
 import Prelude ()
@@ -15,6 +15,8 @@
 import           Game.LambdaHack.Client.UI.MonadClientUI
 import           Game.LambdaHack.Client.UI.Msg
 import           Game.LambdaHack.Client.UI.SessionUI
+import           Game.LambdaHack.Client.UI.TutorialHints
+  (TutorialHints, renderTutorialHints)
 import           Game.LambdaHack.Client.UI.UIOptions
 import           Game.LambdaHack.Common.ActorState
 import           Game.LambdaHack.Common.MonadStateRead
@@ -69,6 +71,10 @@
 -- | Add a message to the current report.
 msgAdd :: (MonadClientUI m, MsgShared a) => a -> Text -> m ()
 msgAdd msgClass t = void $ msgAddDuplicate msgClass t
+
+-- | Add a tutorial hint message to the current report.
+tutorialHintMsgAdd :: MonadClientUI m => TutorialHints -> m ()
+tutorialHintMsgAdd = msgAdd MsgTutorialHint . renderTutorialHints
 
 -- | Add a message to the current report. End previously collected report,
 -- if any, with newline.
diff --git a/engine-src/Game/LambdaHack/Client/UI/Overlay.hs b/engine-src/Game/LambdaHack/Client/UI/Overlay.hs
--- a/engine-src/Game/LambdaHack/Client/UI/Overlay.hs
+++ b/engine-src/Game/LambdaHack/Client/UI/Overlay.hs
@@ -7,6 +7,7 @@
     FontSetup(..), multiFontSetup, singleFontSetup
   , -- * AttrString
     AttrString, blankAttrString, textToAS, textFgToAS, stringToAS
+  , attrStringToString
   , (<+:>), (<\:>)
     -- * AttrLine
   , AttrLine, attrLine, emptyAttrLine, attrStringToAL, firstParagraph
@@ -106,6 +107,10 @@
 stringToAS :: String -> AttrString
 stringToAS = map Color.attrChar1ToW32
 
+-- | Transform 'AttrString' type to 'String'.
+attrStringToString :: AttrString -> String
+attrStringToString = map Color.charFromW32
+
 -- Follows minimorph.<+>.
 infixr 6 <+:>  -- matches Monoid.<>
 (<+:>) :: AttrString -> AttrString -> AttrString
@@ -135,7 +140,7 @@
         Nothing -> False
         Just [] -> True
         Just (c : _) -> isSpace c
-  in any (isPrefixOfNb $ map Color.charFromW32 s) nonbreakableRev
+  in any (isPrefixOfNb $ attrStringToString s) nonbreakableRev
 
 breakAtSpace :: AttrString -> (AttrString, AttrString)
 breakAtSpace lRev =
@@ -163,7 +168,7 @@
 #ifdef WITH_EXPENSIVE_ASSERTIONS
   assert (allB (\ac -> Color.charFromW32 ac /= '\n') s) $  -- expensive in menus
   assert (null s || last s /= Color.spaceAttrW32
-          `blame` map Color.charFromW32 s) $
+          `blame` attrStringToString s) $
     -- only expensive for menus, but often violated by code changes, so disabled
     -- outside test runs
 #endif
diff --git a/engine-src/Game/LambdaHack/Client/UI/Slideshow.hs b/engine-src/Game/LambdaHack/Client/UI/Slideshow.hs
--- a/engine-src/Game/LambdaHack/Client/UI/Slideshow.hs
+++ b/engine-src/Game/LambdaHack/Client/UI/Slideshow.hs
@@ -6,7 +6,7 @@
   , ButtonWidth(..)
   , KYX, xytranslateKXY, xtranslateKXY, ytranslateKXY, yrenumberKXY
   , OKX, emptyOKX, xytranslateOKX, sideBySideOKX, labDescOKX
-  , Slideshow(slideshow), emptySlideshow, unsnoc, toSlideshow
+  , Slideshow(slideshow), emptySlideshow, unsnocSlideshow, toSlideshow
   , attrLinesToFontMap, menuToSlideshow, wrapOKX, splitOverlay, splitOKX
   , highSlideshow
 #ifdef EXPOSE_INTERNAL
@@ -118,8 +118,8 @@
 emptySlideshow :: Slideshow
 emptySlideshow = Slideshow []
 
-unsnoc :: Slideshow -> Maybe (Slideshow, OKX)
-unsnoc Slideshow{slideshow} =
+unsnocSlideshow :: Slideshow -> Maybe (Slideshow, OKX)
+unsnocSlideshow Slideshow{slideshow} =
   case reverse slideshow of
     [] -> Nothing
     okx : rest -> Just (Slideshow $ reverse rest, okx)
diff --git a/engine-src/Game/LambdaHack/Client/UI/TutorialHints.hs b/engine-src/Game/LambdaHack/Client/UI/TutorialHints.hs
new file mode 100644
--- /dev/null
+++ b/engine-src/Game/LambdaHack/Client/UI/TutorialHints.hs
@@ -0,0 +1,65 @@
+module Game.LambdaHack.Client.UI.TutorialHints
+  ( TutorialHints (..)
+  , renderTutorialHints
+  ) where
+
+import Prelude ()
+
+import Game.LambdaHack.Core.Prelude
+
+data TutorialHints =
+    DamageOfDifferentKind
+  | NewFloorNewOpportunity
+  | CannotHarmYouInMelee
+  | CaughtProjectile
+  | HitsWithNoDirectDamage
+  | TemporaryConditions
+  | WokenUpActors
+  | AvoidWalkingEnemies
+  | AlotOfDamageFromOneSource
+  | TerrainNotFullyKnown
+  | OutOfSightEvents
+  | HearingRadius
+  | SwitchTeammate
+  | MeleeEnemies
+  | UseTerrainEffect
+  | SwitchPointmanAndAvoidMeleeAlone
+  | SwitchPointmanAndSoftenFoes
+
+-- | Generate the standard textual representation for the tutorial hints.
+renderTutorialHints :: TutorialHints -> Text
+renderTutorialHints = \case
+  DamageOfDifferentKind ->
+    "You took damage of a different kind than the normal piercing hit, which means your armor couldn't block any part of it. Normally, your HP (hit points, health) do not regenerate, so losing them is a big deal. Apply healing concoctions or take a long sleep to replenish your HP (but in this hectic environment not even uninterrupted resting that leads to sleep is easy)."
+  NewFloorNewOpportunity ->
+    "New floor is new opportunities, though the old level is still there and others may roam it after you left. Viewing all floors, without moving between them, can be done using the '<' and '>' keys."
+  CannotHarmYouInMelee ->
+    "This enemy can't harm you in melee. Left alone could it possibly be of some use?"
+  CaughtProjectile ->
+    "You managed to catch a projectile, thanks to being braced and hitting it exactly when it was at arm's reach. The obtained item has been put into the shared stash of your party."
+  HitsWithNoDirectDamage ->
+    "Some hits don't cause piercing, impact, burning nor any other direct damage. However, they can have other effects, bad, good or both."
+  WokenUpActors ->
+    "Woken up actors regain stats and skills, including sight radius and melee armor, over several turns."
+  AvoidWalkingEnemies ->
+    "To avoid waking enemies up, make sure they don't lose HP nor too much Calm through noises, particularly close ones. Beware, however, that they slowly regenerate HP as they sleep and eventually wake up at full HP."
+  AlotOfDamageFromOneSource ->
+    "You took a lot of damage from one source. If the danger persists, consider retreating towards your teammates or buffing up or an instant escape, if consumables permit."
+  TemporaryConditions ->
+    "Temporary conditions, especially the bad ones, pass quickly, usually after just a few turns. While active, they are listed in the '@' organ menu and the effects of most of them are seen in the '#' skill menu."
+  TerrainNotFullyKnown ->
+    "Solid terrain drawn in pink is not fully known until searched. This is usually done by bumping into it, which also triggers effects and transformations the terrain is capable of. Once revealed, the terrain can be inspected in aiming mode started with the '*' key or with mouse."
+  OutOfSightEvents ->
+    "Events out of your sight radius (as listed in the '#' skill menu) can sometimes be heard, depending on your hearing radius skill. Some, such as death shrieks, can always be heard regardless of skill and distance, including when they come from a different floor."
+  HearingRadius ->
+    "Enemies you can't see are sometimes heard yelling and emitting other noises. Whether you can hear them, depends on their distance and your hearing radius, as listed in the '#' skill menu."
+  SwitchTeammate ->
+    "You survive this mission, or die trying, as a team. After a few moves, feel free to switch the controlled teammate (marked on the map with the yellow box) using the Tab key to another party member (marked with a green box)."  -- assuming newbies don't remap their keys
+  MeleeEnemies ->
+    "Enemies are normally dealt with using melee (by bumping when adjacent) or ranged combat (by 'f'linging items at them)."
+  UseTerrainEffect ->
+    "Enemies can be dealt with not only via combat, but also with clever use of terrain effects, stealth (not emitting nor reflecting light) or hasty retreat (particularly when foes are asleep or drowsy)."
+  SwitchPointmanAndAvoidMeleeAlone ->
+    "When dealing with groups of enemies, remember than you fight as a team. Switch the pointman (marked on the map with the yellow box) using the Tab key until you move each teammate to a tactically advantageous position. Avoid meleeing alone."
+  SwitchPointmanAndSoftenFoes ->
+    "When dealing with groups of armed enemies, remember than you fight as a team. Switch the pointman (marked on the map with the yellow box) using the Tab key until you move each teammate to a tactically advantageous position. Retreat, if necessary to form a front line. Soften the foes with missiles, especially of exploding kind."
diff --git a/engine-src/Game/LambdaHack/Client/UI/UIOptionsParse.hs b/engine-src/Game/LambdaHack/Client/UI/UIOptionsParse.hs
--- a/engine-src/Game/LambdaHack/Client/UI/UIOptionsParse.hs
+++ b/engine-src/Game/LambdaHack/Client/UI/UIOptionsParse.hs
@@ -48,7 +48,10 @@
                 Just _ ->
                   let (key, def) = readError keydef
                   in (K.mkKM key, def :: CmdTriple)
-                Nothing -> configError $ "wrong macro id" `showFailure` ident
+                Nothing ->
+                  configError
+                    $ "macro id should start with Cmd_ and it does not:"
+                      `showFailure` ident
             section = Ini.allItems "additional_commands" cfg
         in map mkCommand section
       uHeroNames =
diff --git a/engine-src/Game/LambdaHack/Client/UI/Watch/WatchSfxAtomicM.hs b/engine-src/Game/LambdaHack/Client/UI/Watch/WatchSfxAtomicM.hs
--- a/engine-src/Game/LambdaHack/Client/UI/Watch/WatchSfxAtomicM.hs
+++ b/engine-src/Game/LambdaHack/Client/UI/Watch/WatchSfxAtomicM.hs
@@ -16,7 +16,7 @@
 import qualified Data.Text as T
 import qualified NLP.Miniutter.English as MU
 
-import           Game.LambdaHack.Atomic
+import           Game.LambdaHack.Atomic (SfxAtomic (..), SfxMsg (..))
 import           Game.LambdaHack.Client.MonadClient
 import           Game.LambdaHack.Client.State
 import           Game.LambdaHack.Client.UI.ActorUI
@@ -35,6 +35,7 @@
 import           Game.LambdaHack.Client.UI.MsgM
 import           Game.LambdaHack.Client.UI.SessionUI
 import           Game.LambdaHack.Client.UI.SlideshowM
+import           Game.LambdaHack.Client.UI.TutorialHints (TutorialHints (..))
 import           Game.LambdaHack.Client.UI.Watch.WatchCommonM
 import           Game.LambdaHack.Common.Actor
 import           Game.LambdaHack.Common.ActorState
@@ -155,7 +156,7 @@
           -- out only in Allure.
           when (isOurCharacter
                 && Ability.getSk Ability.SkArmorMelee actorMaxSk > 0) $
-            msgAdd MsgTutorialHint "You took damage of a different kind than the normal piercing hit, which means your armor couldn't block any part of it. Normally, your HP (hit points, health) do not regenerate, so losing them is a big deal. Apply healing concoctions or take a long sleep to replenish your HP (but in this hectic environment not even uninterrupted resting that leads to sleep is easy)."
+            tutorialHintMsgAdd DamageOfDifferentKind
         feelLookHPGood = feelLook MsgGoodMiscEvent MsgBadMiscEvent
         feelLookCalm bigAdj projAdj = when isAlive $
           feelLook MsgEffectMinor MsgEffectMinor bigAdj projAdj
@@ -260,7 +261,7 @@
               let desc = cdesc $ okind cocave $ lkind lvl
               unless (T.null desc) $
                 msgAdd MsgBackdropInfo $ desc <> "\n"
-              msgAdd MsgTutorialHint "New floor is new opportunities, though the old level is still there and others may roam it after you left. Viewing all floors, without moving between them, can be done using the '<' and '>' keys."
+              tutorialHintMsgAdd NewFloorNewOpportunity
             [] -> return ()  -- spell fizzles; normally should not be sent
       IK.Escape{} | isOurCharacter -> do
         ours <- getsState $ fidActorNotProjGlobalAssocs side
@@ -871,7 +872,7 @@
         tutorialHintBenignFoe =
           when (bfid sb == side
                 && not (actorCanMeleeToHarm actorMaxSkills target tb)) $
-            msgAdd MsgTutorialHint "This enemy can't harm you in melee. Left alone could it possibly be of some use?"
+            tutorialHintMsgAdd CannotHarmYouInMelee
     -- The messages about parrying and immediately afterwards dying
     -- sound goofy, but there is no easy way to prevent that.
     -- And it's consistent.
@@ -883,7 +884,7 @@
                      [MU.SubjectVerbSg spart "catch", tpart, "skillfully"]
          msgAdd MsgSpecialEvent msg
          when (bfid sb == side) $
-           msgAdd MsgTutorialHint "You managed to catch a projectile, thanks to being braced and hitting it exactly when it was at arm's reach. The obtained item has been put into the shared stash of your party."
+           tutorialHintMsgAdd CaughtProjectile
          animate (blid tb) $ blockHit ps Color.BrGreen Color.Green
        | not (hasCharge localTime kitWeapon) -> do
          -- Can easily happen with a thrown discharged item.
@@ -923,7 +924,7 @@
                ++ if bproj sb then [] else weaponNameWith
          msgAdd msgClassMelee msg  -- too common for color
          when (bfid sb == side || bfid tb == side) $
-           msgAdd MsgTutorialHint "Some hits don't cause piercing, impact, burning nor any other direct damage. However, they can have other effects, bad, good or both."
+           tutorialHintMsgAdd HitsWithNoDirectDamage
          animate (blid tb) $ subtleHit ps
        | bproj sb -> do  -- more terse than melee, because sometimes very spammy
          let msgRangedPowerful | targetIsFoe = MsgRangedMightyWe
diff --git a/engine-src/Game/LambdaHack/Client/UI/Watch/WatchUpdAtomicM.hs b/engine-src/Game/LambdaHack/Client/UI/Watch/WatchUpdAtomicM.hs
--- a/engine-src/Game/LambdaHack/Client/UI/Watch/WatchUpdAtomicM.hs
+++ b/engine-src/Game/LambdaHack/Client/UI/Watch/WatchUpdAtomicM.hs
@@ -40,6 +40,7 @@
 import           Game.LambdaHack.Client.UI.MsgM
 import           Game.LambdaHack.Client.UI.SessionUI
 import           Game.LambdaHack.Client.UI.SlideshowM
+import           Game.LambdaHack.Client.UI.TutorialHints (TutorialHints (..))
 import           Game.LambdaHack.Client.UI.UIOptions
 import           Game.LambdaHack.Client.UI.Watch.WatchCommonM
 import           Game.LambdaHack.Client.UI.Watch.WatchQuitM
@@ -137,7 +138,7 @@
                  when (bfid b == side && not good) $
                    -- Others get conditions too often and good ones are not
                    -- dire enough and also too common.
-                   msgAdd MsgTutorialHint "Temporary conditions, especially the bad ones, pass quickly, usually after just a few turns. While active, they are listed in the '@' organ menu and the effects of most of them are seen in the '#' skill menu."
+                   tutorialHintMsgAdd TemporaryConditions
                | otherwise -> do
                  wown <- ppContainerWownW partActorLeader True c
                  itemVerbMU MsgItemCreation iid kit
@@ -186,12 +187,12 @@
   UpdMoveActor aid source target -> moveActor aid source target
   UpdWaitActor aid WSleep _ -> do
     aidVerbMU MsgStatusWakeup aid "wake up"
-    msgAdd MsgTutorialHint "Woken up actors regain stats and skills, including sight radius and melee armor, over several turns."
+    tutorialHintMsgAdd WokenUpActors
   UpdWaitActor aid WWake _ -> do
     side <- getsClient sside
     b <- getsState $ getActorBody aid
     unless (bfid b == side) $
-      msgAdd MsgTutorialHint "To avoid waking enemies up, make sure they don't lose HP nor too much Calm through noises, particularly close ones. Beware, however, that they slowly regenerate HP as they sleep and eventually wake up at full HP."
+      tutorialHintMsgAdd AvoidWalkingEnemies
   UpdWaitActor{} -> return ()  -- falling asleep handled uniformly elsewhere
   UpdDisplaceActor source target -> displaceActorUI source target
   UpdMoveItem iid k aid c1 c2 -> moveItemUI iid k aid c1 c2
@@ -261,7 +262,7 @@
          when (bfid b == side && not (bproj b)) $ do
            when (abs hpDelta >= oneM) $ markDisplayNeeded (blid b)
            when (hpDelta < 0) $ do
-             when (hpDelta <= xM (-3)) $ msgAdd MsgTutorialHint "You took a lot of damage from one source. If the danger persists, consider retreating towards your teammates or buffing up or an instant escape, if consumables permit."
+             when (hpDelta <= xM (-3)) $ tutorialHintMsgAdd AlotOfDamageFromOneSource
              sUIOptions <- getsSession sUIOptions
              currentWarning <-
                getsState $ checkWarningHP sUIOptions aid (bhp b)
@@ -283,7 +284,7 @@
              let bPrev = b {bcalm = bcalm b - calmDelta}
              when (calmEnough b actorMaxSk
                    && not (calmEnough bPrev actorMaxSk)) $
-               msgAdd MsgNeutralEvent "You are again calm enough to manage your equipment outfit."
+               msgAdd MsgSpecialEvent "You are again calm enough to manage your equipment outfit."
            -- If the leader regenerates Calm more often than once per
            -- standard game turn, this will not be reflected, for smoother
            -- and faster display. However, every halt for keypress
@@ -443,7 +444,7 @@
                            , MU.AW object ]
     unless (subject2 == object) $ do
       msgAdd MsgTerrainReveal msg
-      msgAdd MsgTutorialHint "Solid terrain drawn in pink is not fully known until searched. This is usually done by bumping into it, which also triggers effects and transformations the terrain is capable of. Once revealed, the terrain can be inspected in aiming mode started with the '*' key or with mouse."
+      tutorialHintMsgAdd TerrainNotFullyKnown
   UpdHideTile{} -> return ()
   UpdSpotTile{} -> return ()
   UpdLoseTile{} -> return ()
@@ -615,11 +616,11 @@
     msgAdd msgClass msg
     case hearMsg of
       HearUpd UpdDestroyActor{} ->
-        msgAdd MsgTutorialHint "Events out of your sight radius (as listed in the '#' skill menu) can sometimes be heard, depending on your hearing radius skill. Some, such as death shrieks, can always be heard regardless of skill and distance, including when they come from a different floor."
+        tutorialHintMsgAdd OutOfSightEvents
       HearTaunt{} -> do
         globalTime <- getsState stime
         when (globalTime > timeTurn) $  -- avoid too many hints at the start
-          msgAdd MsgTutorialHint "Enemies you can't see are sometimes heard yelling and emitting other noises. Whether you can hear them, depends on their distance and your hearing radius, as listed in the '#' skill menu."
+          tutorialHintMsgAdd HearingRadius
       _ -> return ()
   UpdMuteMessages _ smuteMessages ->
     modifySession $ \sess -> sess {smuteMessages}
@@ -722,7 +723,7 @@
          when born $ do
            let verb = "join you"
            aidVerbMU MsgSpottedActor aid verb
-           msgAdd MsgTutorialHint "You survive this mission, or die trying, as a team. After a few moves, feel free to switch the controlled teammate (marked on the map with the yellow box) using the Tab key to another party member (marked with a green box)."  -- assuming newbies don't remap their keys
+           tutorialHintMsgAdd SwitchTeammate
            animate (blid body) $ actorX (bpos body)
      | otherwise -> do
        -- Don't spam if the actor was already visible
@@ -771,14 +772,14 @@
          case threat of
            ThreatNone -> return ()  -- too rare to care ATM
            ThreatUnarmed ->
-             msgAdd MsgTutorialHint "Enemies are normally dealt with using melee (by bumping when adjacent) or ranged combat (by 'f'linging items at them)."
+             tutorialHintMsgAdd MeleeEnemies
            ThreatArmed ->
-             msgAdd MsgTutorialHint "Enemies can be dealt with not only via combat, but also with clever use of terrain effects, stealth (not emitting nor reflecting light) or hasty retreat (particularly when foes are asleep or drowsy)."
+             tutorialHintMsgAdd UseTerrainEffect
            _ | length friendAssocs <= 1 -> return ()  -- one member on level
            ThreatAnotherUnarmed ->
-             msgAdd MsgTutorialHint "When dealing with groups of enemies, remember than you fight as a team. Switch the pointman (marked on the map with the yellow box) using the Tab key until you move each teammate to a tactically advantageous position. Avoid meleeing alone."
+             tutorialHintMsgAdd SwitchPointmanAndAvoidMeleeAlone
            ThreatAnotherArmed ->
-             msgAdd MsgTutorialHint "When dealing with groups of armed enemies, remember than you fight as a team. Switch the pointman (marked on the map with the yellow box) using the Tab key until you move each teammate to a tactically advantageous position. Retreat, if necessary to form a front line. Soften the foes with missiles, especially of exploding kind."
+             tutorialHintMsgAdd SwitchPointmanAndSoftenFoes
          animate (blid body) $ actorX (bpos body)
 
 destroyActorUI :: MonadClientUI m => Bool -> ActorId -> Actor -> m ()
diff --git a/engine-src/Game/LambdaHack/Common/HighScore.hs b/engine-src/Game/LambdaHack/Common/HighScore.hs
--- a/engine-src/Game/LambdaHack/Common/HighScore.hs
+++ b/engine-src/Game/LambdaHack/Common/HighScore.hs
@@ -25,9 +25,9 @@
 import Game.LambdaHack.Common.Faction
 import Game.LambdaHack.Common.Misc
 import Game.LambdaHack.Common.Time
+import Game.LambdaHack.Content.FactionKind
 import Game.LambdaHack.Content.ItemKind (ItemKind)
 import Game.LambdaHack.Content.ModeKind
-import Game.LambdaHack.Content.FactionKind
 import Game.LambdaHack.Definition.Defs
 
 -- | A single score record. Records are ordered in the highscore table,
@@ -38,7 +38,7 @@
   , date         :: POSIXTime  -- ^ date of the last game interruption
   , status       :: Status     -- ^ reason of the game interruption
   , challenge    :: Challenge  -- ^ challenge setup of the game
-  , gkindName  :: Text       -- ^ name of the faction's gkind
+  , gkindName    :: Text       -- ^ name of the faction's gkind
   , ourVictims   :: EM.EnumMap (ContentId ItemKind) Int  -- ^ allies lost
   , theirVictims :: EM.EnumMap (ContentId ItemKind) Int  -- ^ foes killed
   }
@@ -89,7 +89,10 @@
         HiLoot | dungeonTotal == 0 -> c  -- a fluke; no gold generated
         HiLoot -> c * intToDouble total / intToDouble dungeonTotal
         HiSprint -> -- Up to -c turns matter.
-                    max 0 (-c - turnsSpent)
+          let speedup = max 0 (-c - turnsSpent)
+          in if c >= -10000
+             then speedup  -- every turn matters
+             else 10000 * speedup / (-c)  -- prevent exploit speedruns
         HiBlitz -> -- Up to 1000000/-c turns matter.
                    sqrt $ max 0 (1000000 + c * turnsSpent)
         HiSurvival -> -- Up to 1000000/c turns matter.
diff --git a/engine-src/Game/LambdaHack/Common/Kind.hs b/engine-src/Game/LambdaHack/Common/Kind.hs
--- a/engine-src/Game/LambdaHack/Common/Kind.hs
+++ b/engine-src/Game/LambdaHack/Common/Kind.hs
@@ -8,10 +8,10 @@
   , getKindMean, speedupItem
   , okind, omemberGroup, oisSingletonGroup, ouniqGroup, opick
   , ofoldlWithKey', ofoldlGroup', omapVector, oimapVector
-  , olength, linearInterpolation
+  , olength, linearInterpolation, emptyMultiGroupMode, emptyMultiGroupItem
 #ifdef EXPOSE_INTERNAL
-  , emptyMultiGroupItem, emptyUnknownTile
-  , emptyUIFactionGroupName, emptyMultiGroupMode
+  , emptyUnknownTile
+  , emptyUIFactionGroupName
 #endif
     -- * Operations both internal and used in unit tests
   , emptyUIFaction
diff --git a/test/CommonMUnitTests.hs b/test/CommonMUnitTests.hs
--- a/test/CommonMUnitTests.hs
+++ b/test/CommonMUnitTests.hs
@@ -4,42 +4,16 @@
 
 import Game.LambdaHack.Core.Prelude
 
-import qualified Data.EnumMap.Strict as EM
-import           Test.Tasty
-import           Test.Tasty.HUnit
+import Test.Tasty
+import Test.Tasty.HUnit
 
-import           Game.LambdaHack.Client.CommonM
-import           Game.LambdaHack.Common.Area
-import           Game.LambdaHack.Common.Kind
-import           Game.LambdaHack.Common.Level
-import           Game.LambdaHack.Common.Perception
-import           Game.LambdaHack.Common.Point
-import           Game.LambdaHack.Common.State
-import           Game.LambdaHack.Common.Time
-import           Game.LambdaHack.Content.TileKind
-import qualified Game.LambdaHack.Core.Dice as Dice
+import Game.LambdaHack.Client.CommonM
+import Game.LambdaHack.Common.Kind (emptyCOps)
+import Game.LambdaHack.Common.Perception (emptyPer)
+import Game.LambdaHack.Common.Point (Point (..))
 
 import UnitTestHelpers
-
-testLevel :: Level
-testLevel = Level
-  { lkind = toEnum 0
-  , ldepth = Dice.AbsDepth 1
-  , lfloor = EM.empty
-  , lembed = EM.empty
-  , lbig = EM.empty
-  , lproj = EM.empty
-  , ltile = unknownTileMap (fromJust (toArea (0,0,0,0))) unknownId 10 10
-  , lentry = EM.empty
-  , larea = trivialArea (Point 0 0)
-  , lsmell = EM.empty
-  , lstair = ([],[])
-  , lescape = []
-  , lseen = 0
-  , lexpl = 0
-  , ltime = timeZero
-  , lnight = False
-  }
+  (executorCli, stubCliState, testActor, testLevel, testLevelId)
 
 commonMUnitTests :: TestTree
 commonMUnitTests = testGroup "commonMUnitTests"
diff --git a/test/HandleHumanLocalMUnitTests.hs b/test/HandleHumanLocalMUnitTests.hs
--- a/test/HandleHumanLocalMUnitTests.hs
+++ b/test/HandleHumanLocalMUnitTests.hs
@@ -5,15 +5,22 @@
 import Game.LambdaHack.Core.Prelude
 
 import qualified Data.EnumMap.Strict as EM
-import           Test.Tasty
-import           Test.Tasty.HUnit
+import qualified Data.Text as T
 
+import Test.Tasty
+import Test.Tasty.HUnit
+
+import           Game.LambdaHack.Client.UI (SessionUI (..), modifySession)
 import           Game.LambdaHack.Client.UI.HandleHelperM
 import           Game.LambdaHack.Client.UI.HandleHumanLocalM
 import qualified Game.LambdaHack.Client.UI.HumanCmd as HumanCmd
+import           Game.LambdaHack.Client.UI.Msg
+import           Game.LambdaHack.Client.UI.MsgM
+import           Game.LambdaHack.Client.UI.TutorialHints
 import           Game.LambdaHack.Common.ActorState
 import           Game.LambdaHack.Common.Item
 import           Game.LambdaHack.Common.ItemAspect
+import           Game.LambdaHack.Common.Kind (emptyMultiGroupItem)
 import           Game.LambdaHack.Common.Level
 import           Game.LambdaHack.Common.Point
 import           Game.LambdaHack.Common.PointArray as PointArray
@@ -22,15 +29,11 @@
 import           Game.LambdaHack.Content.TileKind
 import           Game.LambdaHack.Definition.DefsInternal
   (toContentId, toContentSymbol)
-import           Game.LambdaHack.Definition.Flavour
 
 import UnitTestHelpers
 
-stubItem :: Item
-stubItem = Item { jkind = IdentityObvious (toContentId 0), jfid = Nothing, jflavour = dummyFlavour }
-
 testItemFull :: ItemFull
-testItemFull = ItemFull { itemBase = stubItem, itemKindId = toContentId 0, itemKind = testItemKind, itemDisco = ItemDiscoFull emptyAspectRecord, itemSuspect = False }
+testItemFull = ItemFull { itemBase = stubItem, itemKindId = toContentId 0, itemKind = emptyMultiGroupItem, itemDisco = ItemDiscoFull emptyAspectRecord, itemSuspect = False }
 
 handleHumanLocalMUnitTests :: TestTree
 handleHumanLocalMUnitTests = testGroup "handleHumanLocalMUnitTests"
@@ -54,6 +57,25 @@
                    in chooseItemProjectHuman testActorId triggerItems
       result <- executorCli testFn testCliStateWithItem
       showFailError (fromJust (fst result)) @?= "*aiming obstructed by terrain*"
+  , testCase "tutorialHints-msg-in-history-report" $ do
+      let testFn = do
+            modifySession (\sess -> sess {scurTutorial = True})
+              -- permit the client not to ignore tutorial hints
+            tutorialHintMsgAdd CannotHarmYouInMelee
+      result <- executorCli testFn testCliStateWithItem
+      let maybeHistory = shistory <$> (cliSession . snd) result
+      case maybeHistory of
+        Nothing -> assertFailure "History is empty"
+        Just history -> assertBool testFailureMsg isHintThere
+         where
+          renderedNewReports = reportToTexts . newReport $ history
+          renderedHint = renderTutorialHints CannotHarmYouInMelee
+          isHintThere = renderedHint `elem` renderedNewReports
+          testFailureMsg = "Expected to find tutorial hint '"
+            <> (T.unpack . renderTutorialHints $ CannotHarmYouInMelee)
+            <> "' in SessionUI.shistory.newReport '"
+            <> T.unpack (T.unlines renderedNewReports)
+            <> "'"
   , testCase "psuitReq" $  do
       let testFn = psuitReq testActorId
       mpsuitReqMonad <- executorCli testFn testCliStateWithItem
diff --git a/test/ItemDescriptionUnitTests.hs b/test/ItemDescriptionUnitTests.hs
--- a/test/ItemDescriptionUnitTests.hs
+++ b/test/ItemDescriptionUnitTests.hs
@@ -8,58 +8,41 @@
 import           Test.Tasty
 import           Test.Tasty.HUnit
 
-import           Game.LambdaHack.Client.UI.ItemDescription
-import           Game.LambdaHack.Common.Item
-import           Game.LambdaHack.Common.ItemAspect
-import           Game.LambdaHack.Content.ItemKind
-import           Game.LambdaHack.Core.Dice
-import qualified Game.LambdaHack.Definition.Ability as Ability
-import           Game.LambdaHack.Definition.Color
-import           Game.LambdaHack.Definition.Flavour
+import Game.LambdaHack.Client.UI.ItemDescription
+  (viewItem, viewItemBenefitColored)
+import Game.LambdaHack.Common.Item
+import Game.LambdaHack.Common.ItemAspect (emptyAspectRecord)
+import Game.LambdaHack.Common.Kind (emptyMultiGroupItem)
+import Game.LambdaHack.Content.ItemKind (ItemKind (isymbol))
+import Game.LambdaHack.Definition.Color
+  (Color (BrGreen, BrRed, Green), attrChar2ToW32)
+import Game.LambdaHack.Definition.Flavour (zipPlain)
+import UnitTestHelpers (stubItem)
 
 itemDescriptionUnitTests :: TestTree
 itemDescriptionUnitTests = testGroup "itemDescriptionUnitTests" $
-  let greenFlavour = head $ zipPlain [Green]
-      testItemBase = Item
-        { jkind = IdentityObvious (toEnum 667)
-        , jfid = Nothing
-        , jflavour = greenFlavour
-        }
-      testItemKind = ItemKind
-        { isymbol  = 'x'
-        , iname    = "12345678901234567890123"
-        , ifreq    = [ (UNREPORTED_INVENTORY, 1) ]
-        , iflavour = zipPlain [Green]
-        , icount   = 1 + 1 `d` 2
-        , irarity  = [(1, 50), (10, 1)]
-        , iverbHit = "hit"
-        , iweight  = 300
-        , idamage  = 1 `d` 1
-        , iaspects = [ AddSkill Ability.SkHurtMelee $ -16 * 5
-                     , SetFlag Ability.Fragile
-                     , toVelocity 70 ]
-        , ieffects = []
-        , idesc    = "A really cool test item."
-        , ikit     = []
-        }
-      testItemFull = ItemFull
-        { itemBase = testItemBase
+  let testItemFull = ItemFull
+        { itemBase = stubItem
+          {
+            jkind = IdentityObvious (toEnum 667)
+          , jflavour = head $ zipPlain [Green]
+          }
         , itemKindId = toEnum 667
-        , itemKind = testItemKind
+        , itemKind = emptyMultiGroupItem
         , itemDisco = ItemDiscoFull emptyAspectRecord
         , itemSuspect = True
         }
   in
   [ testCase "testItem_viewItem_Blackx" $
-      viewItem testItemFull
+      viewItem testItemFull {itemKind = emptyMultiGroupItem {isymbol = 'x'}}
       @?= attrChar2ToW32 Green 'x'
   , testCase "testItem_viewItem_Black!" $
-      viewItem testItemFull {itemKind = testItemKind {isymbol = '!'}}
+      viewItem testItemFull {itemKind = emptyMultiGroupItem {isymbol = '!'}}
       @?= attrChar2ToW32 Green '!'
   , testCase "testItem_viewItemBenefitColored_isEquip_Greenx" $
-      viewItemBenefitColored (EM.singleton (toEnum 42) (Benefit True 0 0 0 0)) (toEnum 42) testItemFull
+      viewItemBenefitColored (EM.singleton (toEnum 42) (Benefit True 0 0 0 0)) (toEnum 42) testItemFull {itemKind = emptyMultiGroupItem {isymbol = 'x'}}
       @?= attrChar2ToW32 BrGreen 'x'
   , testCase "testItem_viewItemBenefitColored_isNotEquip_Redx" $
-      viewItemBenefitColored (EM.singleton (toEnum 42) (Benefit False 0 0 0 0)) (toEnum 42) testItemFull
+      viewItemBenefitColored (EM.singleton (toEnum 42) (Benefit False 0 0 0 0)) (toEnum 42) testItemFull {itemKind = emptyMultiGroupItem {isymbol = 'x'}}
       @?= attrChar2ToW32 BrRed 'x'
   ]
diff --git a/test/ItemKindUnitTests.hs b/test/ItemKindUnitTests.hs
--- a/test/ItemKindUnitTests.hs
+++ b/test/ItemKindUnitTests.hs
@@ -7,72 +7,53 @@
 import Test.Tasty
 import Test.Tasty.HUnit
 
+import qualified Content.RuleKind
+import           Game.LambdaHack.Common.Kind (emptyMultiGroupItem)
 import           Game.LambdaHack.Content.ItemKind
 import qualified Game.LambdaHack.Content.RuleKind as RK
-import           Game.LambdaHack.Core.Dice
 import qualified Game.LambdaHack.Definition.Ability as Ability
-import           Game.LambdaHack.Definition.Color
-import           Game.LambdaHack.Definition.Flavour
 
-import qualified Content.RuleKind
-
 itemKindUnitTests :: TestTree
 itemKindUnitTests = testGroup "itemKindUnitTests" $
   let standardSymbols = RK.ritemSymbols Content.RuleKind.standardRules
       customSymbols = emptyItemSymbolsUsedInEngine {rsymbolNecklace = '*'}
-      testItemKind = ItemKind
-        { isymbol  = 'x'
-        , iname    = "12345678901234567890123"
-        , ifreq    = [ (UNREPORTED_INVENTORY, 1) ]
-        , iflavour = zipPlain [Green]
-        , icount   = 1 + 1 `d` 2
-        , irarity  = [(1, 50), (10, 1)]
-        , iverbHit = "hit"
-        , iweight  = 300
-        , idamage  = 1 `d` 1
-        , iaspects = [ AddSkill Ability.SkHurtMelee $ -16 * 5
-                     , SetFlag Ability.Fragile
-                     , toVelocity 70 ]
-        , ieffects = []
-        , idesc    = "A really cool test item."
-        , ikit     = []
-        }
+
   in
   [ testCase "overlonginame_validateSingle_errs" $
       validateSingle standardSymbols
-                     testItemKind { iname = "123456789012345678901234" }
+                     emptyMultiGroupItem { iname = "123456789012345678901234" }
       @?= ["iname longer than 23"]
   , testCase "shortEnoughiname_validateSingle_noErr" $
       validateSingle standardSymbols
-                     testItemKind
+                     emptyMultiGroupItem
       @?= []
   , testCase "equipableNoSlotxSymbol_validateSingle_errs" $
       validateSingle standardSymbols
-                     testItemKind { iaspects = [ SetFlag Ability.Equipable ] }
+                     emptyMultiGroupItem { iaspects = [ SetFlag Ability.Equipable ] }
       @?= ["EqpSlot not specified but Equipable or Meleeable and not a likely organ or necklace or template"]
   , testCase "equipableNoSlot,Symbol_validateSingle_noErr" $
       validateSingle standardSymbols
-                     testItemKind { isymbol = ','
+                     emptyMultiGroupItem { isymbol = ','
                                   , iaspects = [ SetFlag Ability.Equipable ] }
       @?= []
   , testCase "equipableNoSlot\"Symbol_validateSingle_noErr" $
       validateSingle standardSymbols
-                     testItemKind { isymbol = '"'
+                     emptyMultiGroupItem { isymbol = '"'
                                   , iaspects = [ SetFlag Ability.Equipable ] }
       @?= []
   , testCase "equipableNoSlot/Symbol_validateSingle_noErr" $
       validateSingle standardSymbols
-                     testItemKind { isymbol = '/'
+                     emptyMultiGroupItem { isymbol = '/'
                                   , iaspects = [ SetFlag Ability.Equipable ] }
       @?= []
   , testCase "equipableNoSlot*CustomRules_validateSingle_noErr" $
       validateSingle customSymbols
-                     testItemKind { isymbol = '*'
+                     emptyMultiGroupItem { isymbol = '*'
                                   , iaspects = [ SetFlag Ability.Equipable ] }
       @?= []
   , testCase "equipableNoSlot\"CustomRules_validateSingle_errs" $
       validateSingle customSymbols
-                     testItemKind { isymbol = '"'
+                     emptyMultiGroupItem { isymbol = '"'
                                   , iaspects = [ SetFlag Ability.Equipable ] }
       @?= ["EqpSlot not specified but Equipable or Meleeable and not a likely organ or necklace or template"]
   ]
diff --git a/test/ItemRevUnitTests.hs b/test/ItemRevUnitTests.hs
--- a/test/ItemRevUnitTests.hs
+++ b/test/ItemRevUnitTests.hs
@@ -12,35 +12,17 @@
 import           Test.Tasty
 import           Test.Tasty.HUnit
 
-import           Game.LambdaHack.Content.ItemKind
-import           Game.LambdaHack.Core.Dice
-import           Game.LambdaHack.Core.Random
-import qualified Game.LambdaHack.Definition.Ability as Ability
-import           Game.LambdaHack.Definition.Color
-import           Game.LambdaHack.Definition.DefsInternal
-import           Game.LambdaHack.Definition.Flavour
-import           Game.LambdaHack.Server.ItemRev
+import Game.LambdaHack.Common.Kind (emptyMultiGroupItem)
+import Game.LambdaHack.Content.ItemKind
+import Game.LambdaHack.Core.Random
+import Game.LambdaHack.Definition.Color
+import Game.LambdaHack.Definition.DefsInternal
+import Game.LambdaHack.Definition.Flavour
+import Game.LambdaHack.Server.ItemRev
 
 itemRevUnitTests :: TestTree
 itemRevUnitTests = testGroup "itemRevUnitTests" $
-  let testItemKind = ItemKind
-        { isymbol  = 'x'
-        , iname    = "12345678901234567890123"
-        , ifreq    = [ (UNREPORTED_INVENTORY, 1) ]
-        , iflavour = zipStory [Black]
-        , icount   = 1 + 1 `d` 2
-        , irarity  = [(1, 50), (10, 1)]
-        , iverbHit = "hit"
-        , iweight  = 300
-        , idamage  = 1 `d` 1
-        , iaspects = [ AddSkill Ability.SkHurtMelee $ -16 * 5
-                     , SetFlag Ability.Fragile
-                     , toVelocity 70 ]
-        , ieffects = []
-        , idesc    = "A really cool test item."
-        , ikit     = []
-        }
-      testItemKind2Flavours = testItemKind
+  let testItemKind2Flavours = emptyMultiGroupItem
         { iflavour = zipStory [Black,Green] }
       emptyIdToFlavourSymbolToFlavourSetPair = ( EM.empty, EM.empty )
       singletonIdToFlavourSymbolToFlavourSetPair =
@@ -51,19 +33,19 @@
   in
   [ testCase "empty & default initializers -> first is single dummy result" $
       let rndMapPair0 = return emptyIdToFlavourSymbolToFlavourSetPair
-          mapPair1 = St.evalState (rollFlavourMap U.empty rndMapPair0 (toContentId 0) testItemKind) $ SM.mkSMGen 1
+          mapPair1 = St.evalState (rollFlavourMap U.empty rndMapPair0 (toContentId 0) emptyMultiGroupItem) $ SM.mkSMGen 1
         in fst mapPair1 @?= EM.singleton (toContentId 0) dummyFlavour
   , testCase "empty & default initializers -> second is empty" $
       let rndMapPair0 = return emptyIdToFlavourSymbolToFlavourSetPair
-          (mapPair1, _) = St.runState (rollFlavourMap U.empty rndMapPair0 (toContentId 0) testItemKind) $ SM.mkSMGen 1
+          (mapPair1, _) = St.runState (rollFlavourMap U.empty rndMapPair0 (toContentId 0) emptyMultiGroupItem) $ SM.mkSMGen 1
         in snd mapPair1 @?= EM.empty
   , testCase "singleton initializers -> first is single dummy result" $
       let rndMapPair0 = return singletonIdToFlavourSymbolToFlavourSetPair
-          (mapPair1, _) = St.runState (rollFlavourMap U.empty rndMapPair0 (toContentId 0) testItemKind) $ SM.mkSMGen 1
+          (mapPair1, _) = St.runState (rollFlavourMap U.empty rndMapPair0 (toContentId 0) emptyMultiGroupItem) $ SM.mkSMGen 1
         in fst mapPair1 @?= EM.singleton (toContentId 0) dummyFlavour
   , testCase "singleton initializers -> second is single dummy result" $
       let rndMapPair0 = return singletonIdToFlavourSymbolToFlavourSetPair
-          (mapPair1, _) = St.runState (rollFlavourMap U.empty rndMapPair0 (toContentId 0) testItemKind) $ SM.mkSMGen 1
+          (mapPair1, _) = St.runState (rollFlavourMap U.empty rndMapPair0 (toContentId 0) emptyMultiGroupItem) $ SM.mkSMGen 1
         in snd mapPair1 @?= EM.singleton 'x' (ES.singleton dummyFlavour)
   , testCase "rollFlavourMap on two flavours -> first flavour can be rolled" $  -- relies on us not messing with RNG
       let rndMapPair0 = return singletonIdToFlavourSymbolToFlavourSetPair
diff --git a/test/LevelUnitTests.hs b/test/LevelUnitTests.hs
--- a/test/LevelUnitTests.hs
+++ b/test/LevelUnitTests.hs
@@ -8,35 +8,10 @@
 import           Test.Tasty
 import           Test.Tasty.HUnit
 
-import           Game.LambdaHack.Common.Area
-import           Game.LambdaHack.Common.Level
-import           Game.LambdaHack.Common.Point
-import           Game.LambdaHack.Common.PointArray as PointArray
-import           Game.LambdaHack.Common.State
-import           Game.LambdaHack.Common.Time
-import qualified Game.LambdaHack.Core.Dice as Dice
-
-import UnitTestHelpers
+import Game.LambdaHack.Common.Level
+import Game.LambdaHack.Common.State
 
-testLevel :: Level
-testLevel = Level
-  { lkind = toEnum 0
-  , ldepth = Dice.AbsDepth 1
-  , lfloor = EM.empty
-  , lembed = EM.empty
-  , lbig = EM.empty
-  , lproj = EM.empty
-  , ltile = PointArray.empty
-  , lentry = EM.empty
-  , larea = trivialArea (Point 0 0)
-  , lsmell = EM.empty
-  , lstair = ([],[])
-  , lescape = []
-  , lseen = 0
-  , lexpl = 0
-  , ltime = timeZero
-  , lnight = False
-  }
+import UnitTestHelpers (testLevel, testLevelId)
 
 testDungeonWithLevel :: State
 testDungeonWithLevel =
diff --git a/test/ReqFailureUnitTests.hs b/test/ReqFailureUnitTests.hs
--- a/test/ReqFailureUnitTests.hs
+++ b/test/ReqFailureUnitTests.hs
@@ -7,46 +7,22 @@
 import Test.Tasty
 import Test.Tasty.HUnit
 
+import qualified Content.RuleKind
 import           Game.LambdaHack.Common.Item
 import           Game.LambdaHack.Common.ItemAspect
+import           Game.LambdaHack.Common.Kind (emptyMultiGroupItem)
 import           Game.LambdaHack.Common.ReqFailure
 import           Game.LambdaHack.Common.Time
 import           Game.LambdaHack.Content.ItemKind
-import           Game.LambdaHack.Core.Dice
-import qualified Game.LambdaHack.Definition.Ability as Ability
-import           Game.LambdaHack.Definition.Color
 import           Game.LambdaHack.Definition.Defs
-import           Game.LambdaHack.Definition.Flavour
-
-import qualified Content.RuleKind
+import           UnitTestHelpers (stubItem)
 
 reqFailureUnitTests :: TestTree
 reqFailureUnitTests = testGroup "reqFailureUnitTests" $
-  let testItemKind = ItemKind
-        { isymbol  = 'x'
-        , iname    = "12345678901234567890123"
-        , ifreq    = [ (UNREPORTED_INVENTORY, 1) ]
-        , iflavour = zipPlain [Green]
-        , icount   = 1 + 1 `d` 2
-        , irarity  = [(1, 50), (10, 1)]
-        , iverbHit = "hit"
-        , iweight  = 300
-        , idamage  = 1 `d` 1
-        , iaspects = [ AddSkill Ability.SkHurtMelee $ -16 * 5
-                     , SetFlag Ability.Fragile
-                     , toVelocity 70 ]
-        , ieffects = []
-        , idesc    = "A really cool test item."
-        , ikit     = []
-        }
-      testItemFull = ItemFull
-        { itemBase = Item
-            { jkind = IdentityObvious (toEnum 667)
-            , jfid = Nothing
-            , jflavour = dummyFlavour
-            }
+  let testItemFull = ItemFull
+        { itemBase = stubItem -- Item { jkind = IdentityObvious (toEnum 667) , jfid = Nothing , jflavour = dummyFlavour}
         , itemKindId = toEnum 667
-        , itemKind = testItemKind
+        , itemKind = emptyMultiGroupItem
         , itemDisco = ItemDiscoFull emptyAspectRecord
         , itemSuspect = True
         }
@@ -58,22 +34,22 @@
       @?= Left ApplyFood
   , testCase "permittedApply: One Skill and , symbol And CGround -> True" $
       permittedApply standardRules timeZero 1 True (Just CGround)
-                     testItemFull {itemKind = testItemKind{isymbol = ','}}
+                     testItemFull {itemKind = emptyMultiGroupItem{isymbol = ','}}
                      quantSingle
       @?= Right True
   , testCase "permittedApply: One Skill and \" symbol -> True" $
       permittedApply standardRules timeZero 1 True Nothing
-                     testItemFull {itemKind = testItemKind{isymbol = '"'}}
+                     testItemFull {itemKind = emptyMultiGroupItem{isymbol = '"'}}
                      quantSingle
       @?= Right True
   , testCase "permittedApply: Two Skill and ? symbol -> FailureApplyRead" $
       permittedApply standardRules timeZero 2 True Nothing
-                     testItemFull {itemKind = testItemKind{isymbol = '?'}}
+                     testItemFull {itemKind = emptyMultiGroupItem{isymbol = '?'}}
                      quantSingle
       @?= Left ApplyRead
   , testCase "permittedApply: Two Skill and , symbol -> True" $
       permittedApply standardRules timeZero 2 True Nothing
-                     testItemFull {itemKind = testItemKind{isymbol = ','}}
+                     testItemFull {itemKind = emptyMultiGroupItem{isymbol = ','}}
                      quantSingle
       @?= Right True
   ]
diff --git a/test/SessionUIMock.hs b/test/SessionUIMock.hs
--- a/test/SessionUIMock.hs
+++ b/test/SessionUIMock.hs
@@ -53,8 +53,8 @@
           in sess { smacroFrame = smacroFrameNew
                   , smacroStack = smacroStackMew }
   case abortOrCmd of
-    Left Looped -> tell [(abuffs, "Macro looped")] >> pure ()
-    Left HeadEmpty -> tell [(abuffs, "")] >> pure ()  -- exit loop
+    Left Looped -> void $ tell [(abuffs, "Macro looped")]
+    Left HeadEmpty -> void $ tell [(abuffs, "")]  -- exit loop
     Right Nothing -> tell [(abuffs, "")] >> humanCommandMock
     Right (Just out) -> tell [(abuffs, show out)] >> humanCommandMock
 
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -66,7 +66,7 @@
       uiOptions = unsafePerformIO $ mkUIOptions corule defClientOptions
       testFontset :: Int -> String -> TestTree
       testFontset n fontsetName =
-        testCase ("SDL fronted; init only; " ++ fontsetName ++ " fontset") $ do
+        testCase ("SDL frontend; init only; " ++ fontsetName ++ " fontset") $ do
           -- This test only works when run from the same directory that
           -- the .cabal file is in. And this is what Debian needs, so OK.
           -- The hacky log priority 0 tells SDL frontend to init
diff --git a/test/UnitTestHelpers.hs b/test/UnitTestHelpers.hs
--- a/test/UnitTestHelpers.hs
+++ b/test/UnitTestHelpers.hs
@@ -4,21 +4,23 @@
   ( CliState(..)
   , emptyCliState
   , executorCli
+  , reportToTexts
   , stubLevel
   , stubState
   , stubCliState
+  , stubItem
   , testActor
   , testActorId
   , testActorWithItem
   , testCliStateWithItem
   , testFactionId
   , testItemId
-  , testItemKind
+  , testLevel
   , testLevelId
 #ifdef EXPOSE_INTERNAL
     -- * Internal operations
-  , CliMock(..)
   , fchanFrontendStub
+  , CliMock(..)
 #endif
   ) where
 
@@ -30,6 +32,7 @@
 import           Control.Monad.Trans.State.Strict
   (StateT (StateT, runStateT), gets, state)
 import qualified Data.EnumMap.Strict as EM
+import qualified Data.Text as Text
 
 import           Game.LambdaHack.Atomic (MonadStateWrite (..))
 import           Game.LambdaHack.Client
@@ -37,34 +40,53 @@
 import           Game.LambdaHack.Client.HandleResponseM
 import           Game.LambdaHack.Client.MonadClient
 import           Game.LambdaHack.Client.State
+  (StateClient (..), TGoal (..), Target (..), emptyStateClient, updateLeader)
 import           Game.LambdaHack.Client.UI
-import           Game.LambdaHack.Client.UI.ActorUI
+  (MonadClientUI (..), SessionUI (..), emptySessionUI)
+import           Game.LambdaHack.Client.UI.ActorUI (ActorUI (..))
 import           Game.LambdaHack.Client.UI.Content.Screen
-import           Game.LambdaHack.Client.UI.ContentClientUI
+  (emptyScreenContent, rheight, rwidth)
+import           Game.LambdaHack.Client.UI.ContentClientUI (coscreen, emptyCCUI)
 import           Game.LambdaHack.Client.UI.Frontend
+  (ChanFrontend (..), FrontReq (..))
 import           Game.LambdaHack.Client.UI.Key (KMP (..))
 import qualified Game.LambdaHack.Client.UI.Key as K
-import           Game.LambdaHack.Client.UI.PointUI
-import           Game.LambdaHack.Client.UI.UIOptions
+import           Game.LambdaHack.Client.UI.Msg
+import           Game.LambdaHack.Client.UI.Overlay
+import           Game.LambdaHack.Client.UI.PointUI (PointUI (..))
+import           Game.LambdaHack.Client.UI.UIOptions (UIOptions (..))
 import           Game.LambdaHack.Common.Actor
-import           Game.LambdaHack.Common.Area
+  (Actor (..), ResDelta (..), Watchfulness (..))
+import           Game.LambdaHack.Common.Area (Area, toArea, trivialArea)
 import           Game.LambdaHack.Common.ClientOptions
-import           Game.LambdaHack.Common.Faction
-import           Game.LambdaHack.Common.Kind
-import           Game.LambdaHack.Common.Level
-import           Game.LambdaHack.Common.Misc
+  (ClientOptions (..), FullscreenMode (..), defClientOptions)
+import           Game.LambdaHack.Common.Faction (Faction (..))
+import           Game.LambdaHack.Common.Item
+import           Game.LambdaHack.Common.Kind (COps (..), emptyUIFaction)
+import           Game.LambdaHack.Common.Level (Level (..))
+import           Game.LambdaHack.Common.Misc (FontSet (..))
 import           Game.LambdaHack.Common.MonadStateRead
-import           Game.LambdaHack.Common.Perception
-import           Game.LambdaHack.Common.Point
+import           Game.LambdaHack.Common.Perception (emptyPer)
+import           Game.LambdaHack.Common.Point (Point (..))
 import           Game.LambdaHack.Common.State
-import           Game.LambdaHack.Common.Time
+  ( State
+  , emptyState
+  , unknownTileMap
+  , updateActorD
+  , updateActorMaxSkills
+  , updateCOpsAndCachedData
+  , updateDungeon
+  , updateFactionD
+  )
+import           Game.LambdaHack.Common.Time (timeZero)
 import           Game.LambdaHack.Common.Types
-import           Game.LambdaHack.Content.ItemKind
-import           Game.LambdaHack.Content.RuleKind
-import           Game.LambdaHack.Content.TileKind
+  (ActorId, FactionId, ItemId, LevelId)
+import           Game.LambdaHack.Content.RuleKind (RuleContent (..))
+import           Game.LambdaHack.Content.TileKind (unknownId)
 import qualified Game.LambdaHack.Core.Dice as Dice
 import qualified Game.LambdaHack.Definition.Ability as Ability
-import           Game.LambdaHack.Definition.Color
+import           Game.LambdaHack.Definition.Color (Color (..))
+import           Game.LambdaHack.Definition.DefsInternal (toContentId)
 import           Game.LambdaHack.Definition.Flavour
 
 -- * UI frontend stub
@@ -128,6 +150,29 @@
                           , fontMono = "mono" })]
   }
 
+stubItem :: Item
+stubItem = Item { jkind = IdentityObvious (toContentId 0), jfid = Nothing, jflavour = dummyFlavour }
+
+testLevel :: Level
+testLevel = Level
+  { lkind = toEnum 0
+  , ldepth = Dice.AbsDepth 1
+  , lfloor = EM.empty
+  , lembed = EM.empty
+  , lbig = EM.empty
+  , lproj = EM.empty
+  , ltile = unknownTileMap (fromJust (toArea (0,0,0,0))) unknownId 10 10  --PointArray.empty
+  , lentry = EM.empty
+  , larea = trivialArea (Point 0 0)
+  , lsmell = EM.empty
+  , lstair = ([],[])
+  , lescape = []
+  , lseen = 0
+  , lexpl = 0
+  , ltime = timeZero
+  , lnight = False
+  }
+
 -- * Stub identifiers
 
 -- Using different arbitrary numbers for these so that if tests fail
@@ -171,7 +216,6 @@
   , ltime = timeZero
   , lnight = False
   }
-
 testFaction :: Faction
 testFaction =
   Faction
@@ -209,25 +253,6 @@
   , bproj = False
   }
 
-testItemKind :: ItemKind
-testItemKind = ItemKind
-  { isymbol  = 'x'
-  , iname    = "12345678901234567890123"
-  , ifreq    = [ (UNREPORTED_INVENTORY, 1) ]
-  , iflavour = zipPlain [Green]
-  , icount   = 1 + 1 `Dice.d` 2
-  , irarity  = [(1, 50), (10, 1)]
-  , iverbHit = "hit"
-  , iweight  = 300
-  , idamage  = 1 `Dice.d` 1
-  , iaspects = [ AddSkill Ability.SkHurtMelee $ -16 * 5
-                , SetFlag Ability.Fragile
-                , toVelocity 70 ]
-  , ieffects = []
-  , idesc    = "A really cool test item."
-  , ikit     = []
-  }
-
 testActorWithItem :: Actor
 testActorWithItem =
   testActor { beqp = EM.singleton testItemId (1,[])}
@@ -293,6 +318,7 @@
 testCliStateWithItem :: CliState
 testCliStateWithItem = stubCliState { cliState = testStateWithItem }
 
+
 -- * Monad harness mock
 
 -- | Client state transformation monad mock.
@@ -328,13 +354,6 @@
     let !newCliS = cliS {cliClient = f $ cliClient cliS}
     in ((), newCliS)
 
--- instance MonadClientSetup CliMock where
---   saveClient = CliMock $ do
---     --toSave <- gets cliToSave
---     cli <- gets cliClient
---     msess <- gets cliSession
---     IO.liftIO $ Save.saveToChan toSave (cli, msess)
-
 instance MonadClientUI CliMock where
   {-# INLINE getsSession #-}
   getsSession f = CliMock $ gets $ f . fromJust . cliSession
@@ -349,22 +368,6 @@
   getCacheBfs = BfsM.getCacheBfs
   getCachePath = BfsM.getCachePath
 
--- instance MonadClientReadResponse CliMock where
---   receiveResponse = CliMock $ do
---     ChanServer{responseS} <- gets cliDict
---     IO.liftIO $ takeMVar responseS
-
--- instance MonadClientWriteRequest CliMock where
---   sendRequestAI scmd = CliMock $ do
---     ChanServer{requestAIS} <- gets cliDict
---     IO.liftIO $ putMVar requestAIS scmd
---   sendRequestUI scmd = CliMock $ do
---     ChanServer{requestUIS} <- gets cliDict
---     IO.liftIO $ putMVar (fromJust requestUIS) scmd
---   clientHasUI = CliMock $ do
---     mSession <- gets cliSession
---     return $! isJust mSession
-
 instance MonadClientAtomic CliMock where
   {-# INLINE execUpdAtomic #-}
   execUpdAtomic _ = return ()  -- handleUpdAtomic, until needed, save resources
@@ -374,3 +377,8 @@
 
 executorCli :: CliMock a -> CliState -> IO (a, CliState)
 executorCli = runStateT . runCliMock
+
+
+-- | Transform 'Report' type to a list of 'Text'.
+reportToTexts :: Report -> [Text.Text]
+reportToTexts report = Text.pack . attrStringToString <$> renderReport False report
