packages feed

MazesOfMonad 1.0.4 → 1.0.5

raw patch · 16 files changed

+300/−76 lines, 16 files

Files

MazesOfMonad.cabal view
@@ -1,10 +1,9 @@ Name:                MazesOfMonad
-Version:             1.0.4
+Version:             1.0.5
 Description:         MazesOfMonad is a console-based Role Playing Game. You create characters
 	with their strong and weak points, and try to complete mazes that are randomly generated. You can pick up gold and items on the way,
 	meet monsters, and deal with them as you want. You can fight, use magic, bribe, trade, steal... This is only a simple game that I did to see what building a 
-	full blown Haskell application is like to develop. Version 1.0 is complete and working, but there's not too many game items as such, I'll build more monsters, spells, 
-	etc.
+	full blown Haskell application is like to develop. Version 1.0.5 is complete and working.
 License:             BSD3
 License-file:        LICENSE
 Author:              JP Moresmau
@@ -29,7 +28,7 @@ 					 ,MoresmauJP.Rpg.Inventory,MoresmauJP.Rpg.InventoryTests,MoresmauJP.Rpg.Items,MoresmauJP.Rpg.ItemsTests
 					 ,MoresmauJP.Rpg.Magic,MoresmauJP.Rpg.MagicTests,MoresmauJP.Rpg.MazeObjects,MoresmauJP.Rpg.MazeObjectsTests
 					 ,MoresmauJP.Rpg.NPC,MoresmauJP.Rpg.NPCTests,MoresmauJP.Rpg.Profile,MoresmauJP.Rpg.ProfileTests
-					 ,MoresmauJP.Rpg.RPG,MoresmauJP.Rpg.Save,MoresmauJP.Rpg.SaveTests,MoresmauJP.Rpg.TextOutput
+					 ,MoresmauJP.Rpg.RPG,MoresmauJP.Rpg.Save,MoresmauJP.Rpg.SaveTests,MoresmauJP.Rpg.Stats,MoresmauJP.Rpg.TextOutput
 					 ,MoresmauJP.Rpg.Trade,MoresmauJP.Rpg.TradeTests
 					 ,MoresmauJP.Util.Lists,MoresmauJP.Util.ListsTests,MoresmauJP.Util.Numbers,MoresmauJP.Util.NumbersTests
 					 ,MoresmauJP.Util.Random,MoresmauJP.Util.RandomTests
TODO.txt view
@@ -1,6 +1,4 @@-- traps in maze (can be detected) 
-	-> will change mazeobjects format hence current games would be lost, maybe try to ensure backwards compatibility
-	-> when level improve, more traps
+
 - npcs can use magic?
 
 - when trading, show which items are better than what we have in hands or on body? Maybe this is too much information...
WHATSNEW.txt view
@@ -1,3 +1,13 @@+1.0.5:
+	Feature: Traps in maze (can be detected) (only in new mazes created with 1.0.5 version)
+		-> when level of character improve, more traps are present
+	Fix: number of items went up instead of down as level went up
+	Fix: Dexterity roll to use shield was easier instead of harder when attacker hit better
+	Fix: Spells to yourself lasted forever instead of expiring
+	Internal: gather statistics on games, tune some difficulty settings
+	Fix: escaping caused wrong map output (some corridors marked as walls)
+	Internal: attacking a NPC first causes its attitude to worsen and become hostile, even if he was friendly at first
+	
 1.0.4:
 	Feature: Mind Potions can restore mental points (only in new mazes created with 1.0.4 version)
 	UI: When meeting and fighting a NPC, you see its attitude and status (physical and mental)
src/Main.hs view
@@ -19,6 +19,7 @@ import MoresmauJP.Util.NumbersTests
 import MoresmauJP.Util.RandomTests
 import MoresmauJP.Rpg.ArenaTests
+import MoresmauJP.Rpg.Stats
 
 import Test.HUnit (runTestTT,Test(TestList),Counts(..)) 
 
@@ -28,6 +29,10 @@ 	gs<-initialGameStateInApp
 	start (WList ["Welcome to Mazes of Monad, JP Moresmau's Role-Playing Game","If you don't know what to type, try help!"], gs)
 	return ()
+
+-- | statistics on a game
+stats :: FilePath -> IO(GameStats)
+stats fp=fileGameStats fp
 
 -- | run unit tests
 test :: IO Test.HUnit.Counts
src/MoresmauJP/Maze1/Maze.hs view
@@ -46,6 +46,13 @@ 		in Just (gw2,win)
 	| otherwise = Nothing	
 
+randomMove :: (MonadRandom m) => GameWorld -> m GameWorld
+randomMove gw=do
+	let expls=explored gw
+	toCell <- randomPickp (DataSet.elems (DataSet.delete (position gw) expls))
+	let expls2=DataSet.union expls (DataSet.fromList (getNeighbours toCell (maze gw)))
+	return (gw{position=toCell,explored=expls2})
+
 isMovePossible :: Cell -> Cell -> Maze -> Bool
 isMovePossible from to mz =  elem to (getNeighbours from mz)
 
src/MoresmauJP/Rpg/Actions.hs view
@@ -13,7 +13,7 @@ 
 initiative = [Dexterity,Willpower]
 melee = [Strength,Dexterity]
-archery = [Dexterity,Perception,Strength]
+--archery = [Dexterity,Perception,Strength]
 conversion=[Charisma,Willpower,Willpower]
 trade=[Charisma,Charisma,Intelligence]
 spellcasting=[Intelligence,Willpower]
@@ -21,8 +21,8 @@ escape=[Dexterity]
 medecine=[Intelligence,Intelligence,Dexterity,Perception]
 pray=[Charisma,Willpower]
-detectTrap=[Perception]
-disableTrap=[Dexterity,Perception]
+detectTrap=[Perception,Perception,Intelligence]
+--disableTrap=[Dexterity,Perception]
 steal=[Dexterity,Dexterity,Intelligence,Perception,Perception]
 
 d20=(1,20)
src/MoresmauJP/Rpg/Arena.hs view
@@ -43,15 +43,15 @@ 	fightInfo::FightInfo
 	} deriving (Show,Read)
 
-screenInteract :: Character -> NPCCharacter -> RandomWrapper -> Int -> IO (ExitState)
-screenInteract c1 c2 rw tc= do
+screenInteract :: Character -> NPCCharacter -> [String]-> RandomWrapper -> Int -> IO (ExitState)
+screenInteract c1 c2 ss rw tc= do
 	let ar=Arena c1 c2 Nothing True [] tc newFightInfo
 	let initialstate=GameState ar (Just (Screen []))
-	evalStateT (evalRandT (screenInteract' c2) rw) initialstate
+	evalStateT (evalRandT (screenInteract' c2 ss) rw) initialstate
 
-screenInteract' :: NPCCharacter -> (RandT (StateT (GameState Arena) IO)) ExitState
-screenInteract' c2 = do
-	let wEncounter=WText (printf "You meet a %s." (ppNPC c2))
+screenInteract' :: NPCCharacter -> [String] -> (RandT (StateT (GameState Arena) IO)) ExitState
+screenInteract' c2 ss= do
+	let wEncounter=combineWidget (WList ss) (WText (printf "You meet a %s." (ppNPC c2)))
 	initial<-getInitialAttitude c2
 	(w,msgs)<-case initial of
 		NPCFight -> do
@@ -59,7 +59,7 @@ 			return (w, (printf "The %s attacks!" (name $ npcCharacter c2)):msgs)
 		NPCWait -> runWriterT chooseAction
 		NPCTrade ->runWriterT tradeInArena
-	let wmsgs=WList msgs
+	let wmsgs=WList ( msgs)
 	let w2=combineWidget wmsgs w
 	commandLoop2 (combineWidget wEncounter w2)
 	(GameState ar2 _)<-get
@@ -154,11 +154,14 @@ 	gs <- get
 	let ar@(Arena {arenaCharacter=c1,arenaOpponent=c2}) = gsData gs
 	((c1b,c2b),rr) <- compete c1 (npcCharacter c2) initiative
+	let att=bindInt (1,8) ((npcAttitude c2)-2)
+	let ar2=ar{arenaCharacter=c1b,arenaOpponent=(c2{npcCharacter=c2b,npcAttitude=att})}
+	put (gs{gsData=ar2})
 	case rr of
 		Success {} -> choiceArena
-		_ ->do
-			put (gs{gsData=ar{arenaCharacter=c1b,arenaOpponent=(c2{npcCharacter=c2b}),chToHit=False}})
-		 	fightInArena'
+		_ -> do
+			put (gs{gsData=ar2{chToHit=False}})
+			fightInArena'
 
 fightInArena' :: WScreenT Arena
 fightInArena' = do
src/MoresmauJP/Rpg/Character.hs view
@@ -103,7 +103,9 @@ 		-- take only 4 best characteristics
 		(take 4 (sortBy (comparing (\ch-> (-(getCharacteristic' c Normal ch)))) allCharacteristics)))
 	+ (div (length $ spells c) 3) -- ponder with spells
-	+ (div (length $ listCarriedItems $ inventory c) 4) -- ponder with items
+	+ (div (length $ listCarriedItems $ inventory c) 3) -- ponder with items
+	- (if (length $ listActiveItems $ inventory c) == 0 then 2 else 0) -- nothing active is a huge problem
+	
 
 mkRating :: Int -> Rating
 mkRating v = Rating (array (Normal,Experience) [(Normal,v),(Current, v),(Experience, 0)])
@@ -176,14 +178,25 @@ 	c1=addCharacteristic' c Current (affected aff) (affect aff)
 	in c1{affects=(aff:a)}
 
+-- | add an affect to the character
+removeAffect :: Character -- ^ the character to affect
+	-> Affect -- ^ the affect
+	-> Character -- ^ the new character
+removeAffect c aff=let
+	c1=addCharacteristic' c Current (affected aff) (-(affect aff))
+	in c1
+
 -- | remove any expired affects on the character
 expireAffects:: Character -- ^ the character to test
 	-> Int  -- ^ the current tick count
 	-> (Character,[String]) -- ^ the new character and the description of lifted effects
 expireAffects c@(Character{affects=a}) tc=let
 	(a',a'')=partition (\x->(untilTick x)>tc) a
-	in (c{affects=a'},map liftDescription a'')
+	c1=foldl removeAffect c a''
+	in (c1{affects=a'},map liftDescription a'')
 	
+	
+	
 recover :: (MonadWriter ScreenMessages m) => Character
 	-> Characteristic
 	-> Int
@@ -222,7 +235,7 @@ restoreWithTimeC c ch1 ch2 tickCount tickToAdd=let 
 	current = getCharacteristic' c Current ch1
 	normal = getCharacteristic' c Normal ch1
-	in if (current<normal) 
+	in if (current>0 && current<normal) 
 		then let
 			current2=getCharacteristic' c Current ch2
 			val=(max 1 (25-current2)) * 20
src/MoresmauJP/Rpg/CharacterTests.hs view
@@ -118,11 +118,12 @@ 	assertEqual "not feel stronger" "you feel stronger" (head descs2)
 	assertEqual "jp4 has not 1 affect left" 1 (length $ affects jp4)
 	assertEqual "jp4 does not have dexterity affect left" aff2 (head $ affects jp4)
+	assertEqual "jp5 has not 10 strength" 10 (getCharacteristic' jp5 Current Strength)
 	
 	assertEqual "descs3 not found while tick is >" 1 (length descs3)
 	assertEqual "not feel clumsy" "you feel as clumsy as usual" (head descs3)
 	assertEqual "jp5 has not 0 affect left" 0 (length $ affects jp5)
-
+	assertEqual "jp5 has not 10 dexterity" 10 (getCharacteristic' jp5 Current Dexterity)
 	
 	))
 	
src/MoresmauJP/Rpg/Fight.hs view
@@ -1,7 +1,7 @@ -- | Fight resolution
 -- (c) JP Moresmau 2009
 module MoresmauJP.Rpg.Fight 
-(giveBlow,FightStatus,swapFS) 
+(giveBlow,FightStatus,swapFS,damageArmor) 
 
 where
 
@@ -123,7 +123,7 @@ 		shieldF :: (MonadRandom m,MonadWriter ScreenMessages m)=> (Character,[ItemType]) -> ItemType -> m (Character,[ItemType])
 		shieldF (c,its) it@(Shield {})=do
 			-- dexterity roll on the shield
-			(c1,rr)<-action c [Dexterity] (subsequentDifficulty g)
+			(c1,rr)<-action c [Dexterity] (-(subsequentDifficulty g))
 			if (isSuccess rr)
 				then do
 					addMessage $ Message(c1,ShieldDefense)
src/MoresmauJP/Rpg/Inventory.hs view
@@ -41,6 +41,12 @@ 		itName::String,
 		spellType::String,
 		price::Gold}
+	| Trap {
+		itName::String,
+		damageLow::Int, 
+		damageHigh::Int,
+		triggerDescription::String
+		}
 	deriving (Show,Read,Eq)
 	
 
src/MoresmauJP/Rpg/Items.hs view
@@ -41,6 +41,11 @@ majorMindPotion=Potion "Major Mind Potion" (MindPotion 9) 30
 totalMindPotion=Potion "Total Mind Potion" (MindPotion 1000) 60
 
+trapDoor=Trap "Trap Door" 3 10 "A trap door opens under your feet!"
+dartLauncher=Trap "Dart Launcher" 2 8 "A dart launches from the wall and hits you!"
+boulderFall=Trap "Boulder" 2 12 "A big boulder falls from the ceiling!"
+
+
 -- occurences of items in the maze
 itemOccurences:: [(ItemType,Int)]
 itemOccurences=	[(sword,12),
@@ -66,6 +71,11 @@ 		(totalMindPotion,1),
 		(Scroll "" "" 40,8)
 	]
+
+trapOccurences :: [(ItemType,Int)]
+trapOccurences = [(trapDoor,4),
+		(dartLauncher,3),
+		(boulderFall,2)]
 	
 isWeapon :: ItemType -> Bool
 isWeapon (Weapon {})=True
@@ -77,6 +87,10 @@ isProtective (Helmet {})=True
 isProtective _ =False
 
+	
+isTrap :: ItemType -> Bool
+isTrap (Trap{})=True
+isTrap _=False	
 	
 canUseItem :: ItemType -> Bool
 canUseItem (Potion {})=True
src/MoresmauJP/Rpg/MazeObjects.hs view
@@ -26,24 +26,26 @@ 
 data MazeOptions = MazeOptions {
 		itemProportion::Int,
-		npcProportion::Int
+		npcProportion::Int,
+		trapProportion::Int
 	}
 	deriving (Read,Show)
 
 generateObjects :: (MonadRandom m)=>Maze -> MazeOptions -> Int -> m MazeObjects
 generateObjects mz options characterLevel =do
-	let ratio=((fromIntegral characterLevel/10.0) ** 2)::Float
+	let ratio=((fromIntegral characterLevel/12.0) ** 2)::Float
 	-- less items as we progress
-	let realItemProportion=(fromIntegral $ itemProportion options) / ratio
+	let realItemProportion=(fromIntegral $ itemProportion options) * ratio
 	itemMap<-generateItems (size mz) realItemProportion
+	let realTrapProportion=(fromIntegral $ trapProportion options) / ratio
+	itemMap2<-generateTraps (size mz) realTrapProportion itemMap
 	npcMap<-generateNPCs mz (fromIntegral $ npcProportion options) characterLevel
-	return (MazeObjects itemMap npcMap)
+	return (MazeObjects itemMap2 npcMap)
 	
 generateItems :: (MonadRandom m)=>Size -> Float -> m (M.Map Cell [ItemType])
 generateItems sz proportion=do
 	cellsWithItems<-getCellWithItems sz proportion
 	let occ=occurenceList itemOccurences
-	--print occ
 	foldM (\m cell -> do
 		it <- generateScroll =<< randomPickp occ
 		let m2=case M.lookup cell m of
@@ -57,10 +59,23 @@ 	return (Scroll (printf "Scroll of %s" (spellName s)) (spellName s) g)
 generateScroll it=return it
 
+generateTraps :: (MonadRandom m)=>Size -> Float -> (M.Map Cell [ItemType]) -> m (M.Map Cell [ItemType])
+generateTraps sz proportion m=do
+	cellsWithItems<-getCellWithItems sz proportion
+	let occ=occurenceList trapOccurences
+	foldM (\m cell -> do
+		it <- randomPickp occ
+		let m2=case M.lookup cell m of
+			Nothing->M.insert cell [it] m
+			Just its->M.insert cell (it:its) m
+		return m2) m cellsWithItems
+		
+
 generateNPCs :: (MonadRandom m)=> Maze -> Float -> Int -> m (M.Map Cell NPCCharacter)
 generateNPCs mz proportion characterLevel=do
 	cellsWithItems<-(getCellWithItems (size mz) proportion) 
 	let attitudeModifier=floor ((fromIntegral characterLevel/10.0) ** 2)
+	-- no npc on start cell
 	let cells2=delete (start mz) cellsWithItems
 	let occ=occurenceList (getNPCOccurences characterLevel)
 	foldM (\m cell -> do
@@ -83,8 +98,19 @@ 		celly<-getRandomRange (1,h)
 		return (cellx,celly)
 		
-listItems :: GameWorld -> MazeObjects -> [ItemType]
-listItems gw mo =fromMaybe [] $ M.lookup (position gw) (items mo)
+listPickableItems :: GameWorld -> MazeObjects -> [ItemType]
+listPickableItems gw mo=(filter (not . isTrap)) $ listItems' gw mo
+
+getTrap:: GameWorld -> MazeObjects -> (MazeObjects,Maybe ItemType)
+getTrap gw mo=let
+ 	maybeTrap=listToMaybe $ (filter isTrap) $ listItems' gw mo
+	in case maybeTrap of
+		Nothing->(mo,Nothing)
+		Just it->((MoresmauJP.Rpg.MazeObjects.takeItem gw mo it),Just it)
+
+listItems':: GameWorld -> MazeObjects -> [ItemType]
+listItems' gw mo =fromMaybe [] $ M.lookup (position gw) (items mo)
+
 
 takeItem :: GameWorld -> MazeObjects -> ItemType -> MazeObjects
 takeItem gw mo it= 
src/MoresmauJP/Rpg/NPC.hs view
@@ -128,6 +128,23 @@ 	possibleGold=(0,0)	
 	}	
 	
+scorpion=NPCTemplate {
+	typeName="Giant Scorpion",
+	creatureType=Animal,
+	attitudeRange=(4,8),
+	traitRanges=[
+		(Strength,(12,18)),
+		(Dexterity,(14,20)),
+		(Constitution,(15,25)),
+		(Willpower,(12,16)),
+		(Intelligence,(4,6)),
+		(Balance,(10,12)),
+		(Charisma,(1,4)),
+		(Perception,(10,14))],
+	possibleItems=[],
+	possibleGold=(0,0)	
+	}		
+	
 vampire=NPCTemplate {
 	typeName="Vampire",
 	creatureType=Humanoid,
@@ -218,9 +235,10 @@ 			(Just battleaxe,1)]
 			),
 		(LeftHand, [
-			(Nothing,2),
+			(Nothing,1),
 			(Just smallShield,2),
-			(Just bigShield,1)
+			(Just bigShield,1),
+			(Just dagger,2)
 			]
 			),
 		(Body, [
@@ -230,12 +248,43 @@ 			]
 			),
 		(Head,[
-			(Nothing, 3),
+			(Nothing, 1),
 			(Just helmet,2),
 			(Just heaume,1)
 			])],
 	possibleGold=(10,30)	
 	}
+	
+darkElf=NPCTemplate {
+	typeName="Dark Elf",
+	creatureType=Human,
+	attitudeRange=(5,10),	
+	traitRanges=[
+		(Strength,(12,18)),
+		(Dexterity,(16,22)),
+		(Constitution,(10,16)),
+		(Willpower,(10,15)),
+		(Intelligence,(15,22)),
+		(Balance,(10,14)),
+		(Charisma,(12,20)),
+		(Perception,(16,22))],
+	possibleItems=[(RightHand,[
+			(Just sword,3),
+			(Just dagger,1),
+			(Just hatchet,1)]
+			),
+		(LeftHand, [
+			(Nothing,2),
+			(Just sword,2),
+			(Just dagger,4),
+			(Just hatchet,4)]
+			),
+		(Body, [
+			(Just leatherArmor,1),
+			(Nothing,2)	]
+			)],
+	possibleGold=(10,30)	
+	}	
 
 minotaur=NPCTemplate {
 	typeName="Minotaur",
@@ -283,7 +332,8 @@ 
 
 allNPCTemplates :: [NPCTemplate]
-allNPCTemplates= [troll,goblin,kobold,giantRat,snake,vampire,ghoul,outlaw,blackKnight,minotaur,peddler]
+allNPCTemplates= [troll,goblin,kobold,giantRat,snake,scorpion,
+	vampire,ghoul,outlaw,blackKnight,minotaur,peddler,darkElf]
 
 templateAttitudeNoChange=[peddler]
 
@@ -292,7 +342,7 @@ 	let 
 		totalTemplates=length allNPCTemplates
 		ponderedLevel=div (characterLevel * 8) 10
-		likelyhood npc=(npc,totalTemplates - (abs ((templateLevel npc) - ponderedLevel)))
+		likelyhood npc=(npc,max 1 (totalTemplates - (2 * (abs ((templateLevel npc) - ponderedLevel)))))
 	in map likelyhood allNPCTemplates
 		
 templateLevel :: NPCTemplate -> Int
src/MoresmauJP/Rpg/RPG.hs view
@@ -8,14 +8,15 @@ import Data.Char
 import Data.List
 import Data.Maybe
-import qualified Data.Set as DataSet
 
 import MoresmauJP.Core.Screen
 import MoresmauJP.Maze1.Maze
 import qualified MoresmauJP.Maze1.MazeGame as MG
 
+import MoresmauJP.Rpg.Actions
 import MoresmauJP.Rpg.Arena
 import MoresmauJP.Rpg.Character
+import MoresmauJP.Rpg.Fight
 import MoresmauJP.Rpg.Items
 import MoresmauJP.Rpg.NPC
 import MoresmauJP.Rpg.Profile
@@ -182,7 +183,8 @@ mazeOptions :: MazeOptions
 mazeOptions = MazeOptions {
 	itemProportion=4,
-	npcProportion=3
+	npcProportion=3,
+	trapProportion=10
 }
 
 
@@ -197,17 +199,12 @@ createMazeState' :: Character -> RPGGameState -> WScreenT RPGState
 createMazeState' c rgs = do
 	(RPGState {fp=fp}) <- gets gsData
-	s<-getSplit
 	let rpg2=RPGState (Just c) (Just rgs) fp
-	(rpg3,wGold)<-liftIO $ processFight rpg2 s
-	let ms=getMazeScreen rpg3
-	let gs2=GameState rpg3 ms 
+	let ms=getMazeScreen rpg2
+	let gs2=GameState rpg2 ms 
 	put gs2
-	w<-if isJust ms
-			then mapAction []
-			else do
-				return (WText "Game over")
-	saveGameAndCharacter (combineWidget wGold w)
+	w<-mapAction []
+	saveGameAndCharacter w
 
 getMazeScreen :: RPGState  -> Maybe (Screen RPGState)
 getMazeScreen RPGState{rpgCharacter=Nothing}  = Nothing
@@ -224,7 +221,7 @@ 		statusA=Action "character" "See your current character characteristics" statusAction
 		bckA= Action "archive" "Save a backup of your game and character" saveBackupAction
 		acts=bckA:statusA:mapA:invA:(backAction $ getGameScreen initialScreen):actions
-		items=Objs.listItems (mazegameworld mgs) (objects mgs)
+		items=Objs.listPickableItems (mazegameworld mgs) (objects mgs)
 		acts2=if null items
 			then acts
 			else (Action "pickup" "Pick up an item" (pickupAction items)):acts
@@ -252,7 +249,7 @@ getItems (RPGState {mgs=Nothing})= WNothing
 getItems (RPGState {mgs=Just mgs})=	
 	let 
-		items=Objs.listItems (mazegameworld mgs) (objects mgs)
+		items=Objs.listPickableItems (mazegameworld mgs) (objects mgs)
 	in WList ("You see:": 
 			if null items 
 				then ["Nothing"]
@@ -274,41 +271,74 @@ 	let gw=mazegameworld rmgs
 	sg <- getSplit
 	((WList s,_),(GameState gw2 sc))<- liftIO $ runStateT (evalRandT (runWriterT $ MG.move dir []) sg) (GameState gw Nothing)
-	let nameC=name $ c1
 	if (isJust sc) 
 		then
 			do
 				rs2<- tick 10 rs{mgs=Just rmgs{mazegameworld=gw2}}
-				srw<-getSplit
-				(rs3,wGold)<-liftIO $ processFight rs2 srw
-				let ms=getMazeScreen rs3
-				let gs2=GameState rs3 ms 
-				let (WList itemNames)=getItems rs3
-				put gs2
-				w<-if isJust ms
-						-- we have escaped, probably
-						then if (position $ mazegameworld $ fromJust $ mgs rs3)== (position $ mazegameworld $ fromJust $ mgs rs2)
-							then return (WList (s++itemNames))
-							else mapAction []
-						else do
-							return (WText "Game over")
-				saveGameAndCharacter (combineWidget wGold w)
+				let (objects2,trap)=Objs.getTrap (gw2) (objects rmgs)
+				case trap of
+					Nothing -> checkFight rs2 [] s
+					Just trap-> do
+						let rs3=rs2{mgs=Just (fromJust $ mgs rs2){objects=objects2}}
+						(c2,rr)<-action c1 detectTrap (toIntLevel RatherHard)
+						case rr of 
+							Success {}-> do
+								checkFight rs3 [(printf "You avoid a %s" (itName trap))] s							
+							Failure {grade=gr}-> do
+								--addScreenMessage (triggerDescription trap)
+								let s2=[(triggerDescription trap)]
+								dmg<-roll (damageLow trap, damageHigh trap)
+								(c3,armor)<-damageArmor c2 gr
+								let diff=max 0 (dmg-armor)
+								if (diff>0) 
+									then do
+										let s3=s2++[(printf "You lose %s!" (points Physical diff))]
+										--addScreenMessage (printf "You lose %s!" (points Physical diff))
+										let c4=addCharacteristic' c3 Current Physical (-diff)
+										let rs4=rs3{rpgCharacter=Just c4}
+										let ms=getMazeScreen rs4
+										if isJust ms
+											then checkFight rs4 s3 s
+											else do
+												let gs2=GameState rs4 ms
+												put gs2
+												saveGameAndCharacter (WList  (s3 ++ ["Game over"]))
+									else 
+										checkFight rs3 (s2 ++ ["Your armor protects you."]) s
 		else
 			do
+				let nameC=name $ c1
 				liftIO $ deleteGame rs nameC
 				put (GameState (RPGState Nothing Nothing fp) (Just (getGameScreen initialScreen)))
 				return (WList s)	
 
-processFight :: RPGState -> RandomWrapper -> IO (RPGState,Widget RPGState)
-processFight rs@(RPGState{rpgCharacter=Nothing}) _=return (rs,WNothing)
-processFight rs@(RPGState{mgs=Nothing}) _=return (rs,WNothing)
-processFight rs@(RPGState{rpgCharacter=Just c,mgs=Just mgs}) rw= do
+checkFight :: RPGState -> [String] -> [String] -> WScreenT RPGState
+checkFight rs2 pre post= do
+	srw<-getSplit
+	(rs3,wGold)<-liftIO $ processFight rs2 pre srw
+	let ms=getMazeScreen rs3
+	let gs2=GameState rs3 ms 
+	let (WList itemNames)=getItems rs3
+	put gs2
+	w<-if isJust ms
+			-- we have escaped, probably
+			then if (position $ mazegameworld $ fromJust $ mgs rs3)== (position $ mazegameworld $ fromJust $ mgs rs2)
+				then return (WList (post++itemNames))
+				else mapAction []
+			else do
+				return (WText "Game over")
+	saveGameAndCharacter (combineWidget wGold w)
+
+processFight :: RPGState -> [String] -> RandomWrapper -> IO (RPGState,Widget RPGState)
+processFight rs@(RPGState{rpgCharacter=Nothing}) _ _=return (rs,WNothing)
+processFight rs@(RPGState{mgs=Nothing}) _ _ =return (rs,WNothing)
+processFight rs@(RPGState{rpgCharacter=Just c,mgs=Just mgs}) pre rw= do
 	let npc=Objs.getNPC (mazegameworld mgs) (objects mgs)
 	if (isJust npc)
 		then do
 			let npc'=fromJust npc
 			let (rw1,rw2)=splitWrapper rw
-			es <- screenInteract c npc' rw1 (tickCount mgs)
+			es <- screenInteract c npc' pre rw1 (tickCount mgs)
 			(mo,c2,extraGold) <- case (exitStatus es) of
 				Victory -> return $ killNPC (mazegameworld mgs) (objects mgs) (exitCharacter es)
 				EnnemyFlight -> do
@@ -319,8 +349,7 @@ 			gw<-case (exitStatus es) of
 				Flight -> do
 					let gw=mazegameworld mgs
-					pos <- evalRandT (randomPickp (DataSet.elems (DataSet.delete (position gw) (explored gw)))) rw2
-					return (gw{position=pos})
+					evalRandT (randomMove gw) rw2
 				_ -> return (mazegameworld mgs)
 			let mgs2=mgs{mazegameworld=gw,objects=mo2,tickCount=exitTickCount es}
 			let rs2=rs{rpgCharacter=Just c2,mgs=Just mgs2}
@@ -329,7 +358,7 @@ 				else WNothing
 			return (rs2,w)
 		else
-			return (rs,WNothing)
+			return (rs,WList pre)
 
 
 
@@ -399,9 +428,11 @@ 	let 
 		Just c=rpgCharacter rs
 		currentTc=tickCount mgs
-		c2=restoreWithTime c currentTc tc
+		newTick=currentTc+tc
+		(c2,s)=expireAffects (restoreWithTime c currentTc tc) newTick
 		mo2=recoverNPCWithTime (objects mgs) currentTc tc
-		rs2=rs{rpgCharacter=Just c2,mgs=(Just mgs{objects=mo2,tickCount=currentTc+tc})}
+		rs2=rs{rpgCharacter=Just c2,mgs=(Just mgs{objects=mo2,tickCount=newTick})}
+	mapM addScreenMessage s
 	saveNewGameState rs2
 	return rs2
 
+ src/MoresmauJP/Rpg/Stats.hs view
@@ -0,0 +1,61 @@+
+module MoresmauJP.Rpg.Stats where
+
+import qualified Data.Map as M
+import Data.Maybe
+
+import MoresmauJP.Maze1.Maze
+
+import MoresmauJP.Rpg.Character
+import MoresmauJP.Rpg.Items
+import MoresmauJP.Rpg.MazeObjects
+import MoresmauJP.Rpg.NPC
+import MoresmauJP.Rpg.Save
+
+import MoresmauJP.Util.Numbers
+
+import System.Directory
+import System.FilePath
+import System.IO
+
+data GameStats = GameState {
+	charLevel::Int
+	,numberOfNPCs::Int
+	,proportionNPCs::Int
+	,averageNPCLevel::Int
+	,averageNPCAttitude::Int
+	,numberOfItems::Int
+	,proportionItems::Int
+	,numberOfTraps::Int
+	,proportionTraps::Int
+	} deriving (Show,Read)
+
+gameStats :: RPGState -> GameStats
+gameStats rs=let
+	chLevel=characterLevel (fromJust $ rpgCharacter rs)
+	mgw=mazegameworld $ fromJust $ mgs rs
+	(h,w)=size $ maze mgw
+	cellCount=h*w
+	allNPCs=M.elems $ npcs $ objects $ fromJust $ mgs rs
+	npcCount=length allNPCs
+	npcProp=div cellCount npcCount
+	avgAttitude=avg (map npcAttitude allNPCs)
+	avgLevel=avg (map (characterLevel . npcCharacter) allNPCs)
+	allItems=(filter (not . isTrap)) $ concat $ M.elems $ items $ objects $ fromJust $ mgs rs
+	itemCount=length allItems
+	itemProp=div cellCount itemCount
+	allTraps=(filter isTrap) $ concat $ M.elems $ items $ objects $ fromJust $ mgs rs
+	trapCount=length allTraps
+	trapProp=div cellCount trapCount		
+	in GameState chLevel npcCount npcProp avgLevel avgAttitude itemCount itemProp trapCount trapProp
+	
+fileGameStats :: String -> IO GameStats
+fileGameStats name=do
+	dir<-getAppUserDataDirectory "MazesOfMonad"
+	let fp=dir</> (makeValid $ (addExtension name backupExtension))
+	withFile fp ReadMode (\h -> do
+		s<-hGetContents h
+		c<-(readIO s)::IO BackupState
+		let rs=fromBackupState (RPGState Nothing Nothing (takeDirectory fp)) c
+		return (gameStats rs)
+		)