\documentclass[a4paper]{article} \usepackage[latin1]{inputenc} \usepackage[german]{babel} \usepackage{hyperref} \usepackage[top=2cm, left=2cm, right=2cm, bottom=2cm, a4paper]{geometry} \usepackage{tikz} \usepackage{haskell} \lstset{firstnumber=last} \title{Hangman-AI in Haskell} \date{10.7.2006} \author{Joachim Breitner} \begin{document} \maketitle \begin{abstract} In meinem \href{http://www.joachim-breitner.de/wiki/Info\_II\_Tutorium\_SS\_06}{Informatik-II-Tutorium} habe ich heute (mangels besseren Themen) gemeinsam mit meien Tutoranten ein Programm geschrieben, das relativ gut Hangman-Spiele, auch bekannt als Galgenmännchen, lösen können sollte. \end{abstract} \section*{Übersicht} Die Logik des Programme ist folgende: Wir haben eine Liste von möglichen Wörtern. Aus dieser wählen wir jene aus, die nach dem aktuellen Wissenstandt (dem "`Pattern"') in Frage kommen und schauen, welche noch nicht gefragte Buchstabe kommt am häufigsten vor, und den Raten wir dann. \section*{Die Worliste} Die Wortliste habe ich auf meinem Debian-Sytem wie folgt generiert: \begin{lstlisting}[language=bash,numbers=none] cat /usr/share/dict/words | iconv -f latin1 -t utf8 | perl -n -p -e 's/ä/ae/ig; s/ö/oe/ig; s/ü/ue/ig;s/ß/ss/g;' > words \end{lstlisting} \section*{Der Code} Erst einmal laden wir einige nützliche Funktionen \begin{code} import IO import Char import List \end{code} Dann brauchen wir eine Funktion, die schaut, ob ein gegebenes Wort (3. Parameter) noch in das Pattern (2. Parameter) passt, wobei Buchstaben, die im 1. Paramter vorkommen, nicht mehr hinzukommen dürfen. \begin{code} match seen [] [] = True match seen [] _ = False match seen _ [] = False match seen ('_':ps) (b:ws) | b `elem` seen = False -- Der Unterstrich ist der Platzhalter | otherwise = match seen ps ws match seen (a:ps) (b:ws) = a == b && match seen ps ws \end{code} Eine weitere Funktion durchsucht die Worliste nach den Wörtern, die zum Pattern passen. \begin{code} myfilter pat list seen = filter (match seen pat) list \end{code} \lstinline-countb- ist eine Abkürzung für \lstinline-lenght $ filter (==b)-, die hoffentlich etwas schneller ist. \begin{code} countb _ [] = 0 countb b (x:xs) | b == x = 1 + countb b xs | otherwise = countb b xs \end{code} Diese Funktion ist aus dem Übungsbetrieb bekannt, hier jedoch etwas effizienter implementiert, da die rekursive Variante, wie sie auf Abgaben vorkam, bei unserer Wortliste zu einem Stack Overflow führte. Das Ergebnis ist eine Liste von Tupeln mit der Häufigkeit des Buchstabens und dem Buchstaben selbst. \begin{code} count stat text seen = [ (countb b text, b) | b <- ['A'..'Z'], b `notElem` seen ] \end{code} Hier ist die Hauptfunktion, welche rekursiv für jeden Zug ausgeführt wird. Zuerst fragen wir nach dem Pattern, schauen, ob es überhaupt zum vorherigen passt (um ggf. nochmal zu fragen). Dann verkleinern wir die Worliste auf die noch möglichen Wörter. Bliebt nix mehr übrig, geben wir auf, kommt nur noch ein Wort in Frage, so geben wir dieses aus, ansonsten nehmen wir den häufigsten Buchstaben, und rufen uns selbst wieder rekursiv auf. \begin{code} denken liste seen' oldpat = do putStrLn $ "Wie sieht das Pattern denn grad aus?" pat' <- getLine let pat = map toUpper pat' -- nachsichtige Eingabe if not $ null oldpat || match [] oldpat pat -- oldpat ist am Anfang leer then do putStrLn "Nein, ich esse mein Pattern nicht, mein Pattern ess ich nicht!" denken liste seen' oldpat -- Wiederholung der Eingabe else do -- Zeichen im Pattern sind "`gesehen"' let seen = seen' ++ [ b | b <- pat, b /= '_', b `notElem` seen'] let filtered = myfilter pat liste seen if null filtered -- keine Ahnung mehr then putStrLn "Ich gebe auf..." else do if all (/='_') pat' -- Keine unbekannten mehr then putStr "Juhuuuh.\n" else do if null $ tail filtered -- nur noch ein Kandidat then putStrLn $ "Ich löse: "++(head filtered) else do if length filtered < 5 -- kleine Ausgabe bei wenig Kandidaten then putStr $ "Hmm, eigentlich nur: \n"++(unlines $ map (" "++) filtered) else return () let (_,guess) = last $ sort $ count [] (concat filtered) seen putStrLn $ "Ich kaufe ein "++(show guess) denken filtered (guess:seen') pat -- und weiterraten \end{code} Das Hauptprogramm läd die Wortlistendatei und macht daraus eine Liste von Wörtern aus Großbuchstaben, um dann obige Funktion ohne Pattern und gesehenen Buchstaben aufruft. \begin{code} main = do wortliste <- readFile "words" let woerter = map (map toUpper) $ lines wortliste denken woerter [] [] \end{code} \section*{Fazit} Das Ergebnis war zufriedenstellend. Das Programm war zwar nicht schnell, aber die Wartezeite nwaren nicht zu lange (dramaturgisch eher besser), und die Rategenauigkeit war gut. So brauchte das versammelte Tutorium für "`OMEGA"' 10 Buchstaben, das Programm nur 7\ldots \end{document}