-
Notifications
You must be signed in to change notification settings - Fork 2
/
TicTacToe.hs
234 lines (195 loc) · 7.9 KB
/
TicTacToe.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
module TicTacToe where
import Graphics.Gloss.Interface.Pure.Game
import Data.List (transpose)
-- | Запустить игру «Крестики-нолики».
runTicTacToe :: IO ()
runTicTacToe = do
play display bgColor fps initGame drawGame handleGame updateGame
where
display = InWindow "Крестики-нолики" (screenWidth, screenHeight) (200, 200)
bgColor = black -- цвет фона
fps = 60 -- кол-во кадров в секунду
-- =========================================
-- Модель игры
-- =========================================
-- | Фишки игроков.
data Mark = X | O
deriving (Eq, Show)
-- | Клетка игрового поля.
type Cell = Maybe Mark
-- | Игровое поле.
type Board = [[Cell]]
-- | Состояние игры.
data Game = Game
{ gameBoard :: Board -- ^ Игровое поле.
, gamePlayer :: Mark -- ^ Чей ход?
, gameWinner :: Maybe Mark -- ^ Победитель.
}
-- | Начальное состояние игры.
-- Игровое поле — пусто.
-- Первый игрок ходит за крестики.
initGame :: Game
initGame = Game
{ gameBoard = replicate boardHeight (replicate boardWidth Nothing)
, gamePlayer = X
, gameWinner = Nothing
}
-- =========================================
-- Отрисовка игры
-- =========================================
-- | Отобразить игровое поле.
drawGame :: Game -> Picture
drawGame game = translate (-w) (-h) (scale c c (pictures
[ drawGrid
, drawBoard (gameWinner game) (gameBoard game)
]))
where
c = fromIntegral cellSize
w = fromIntegral screenWidth / 2
h = fromIntegral screenHeight / 2
-- | Сетка игрового поля.
drawGrid :: Picture
drawGrid = color white (pictures (hs ++ vs))
where
hs = map (\j -> line [(0, j), (n, j)]) [1..m - 1]
vs = map (\i -> line [(i, 0), (i, m)]) [1..n - 1]
n = fromIntegral boardWidth
m = fromIntegral boardHeight
-- | Нарисовать фишки на игровом поле.
drawBoard :: Maybe Mark -> Board -> Picture
drawBoard win board = pictures (map pictures drawCells)
where
drawCells = map drawRow (zip [0..] board)
drawRow (j, row) = map drawCellAt (zip [0..] row)
where
drawCellAt (i, cell) = translate (0.5 + i) (0.5 + j)
(drawCell (estimate board) win cell)
-- | Нарисовать фишку в клетке поля (если она там есть).
drawCell :: (Int, Int) -> Maybe Mark -> Cell -> Picture
drawCell _ _ Nothing = blank
drawCell (x, o) win (Just mark)
= color markColor (drawMark mark)
where
markColor
| win == Just mark = light orange
| otherwise = case mark of
X | x < o -> greyN (max 0.5 (1 - fromIntegral (o - x) / 10))
O | x > o -> greyN (max 0.5 (1 - fromIntegral (x - o) / 10))
_ -> white
-- | Нарисовать фишку.
drawMark :: Mark -> Picture
drawMark X = drawX
drawMark O = drawO
-- | Нарисовать «крестик».
drawX :: Picture
drawX = pictures
[ polygon [(-0.4, 0.3), (-0.3, 0.4), ( 0.4, -0.3), ( 0.3, -0.4)]
, polygon [(-0.4, -0.3), (-0.3, -0.4), ( 0.4, 0.3), ( 0.3, 0.4)] ]
-- | Нарисовать «нолик».
drawO :: Picture
drawO = thickCircle 0.3 0.1
-- =========================================
-- Обработка событий
-- =========================================
-- | Обработка событий.
handleGame :: Event -> Game -> Game
handleGame (EventKey (MouseButton LeftButton) Down _ mouse) = placeMark (mouseToCell mouse)
handleGame _ = id
-- | Поставить фишку и сменить игрока (если возможно).
placeMark :: (Int, Int) -> Game -> Game
placeMark (i, j) game =
case gameWinner game of
Just _ -> game -- если есть победитель, то поставить фишку нельзя
Nothing -> case modifyAt j (modifyAt i place) (gameBoard game) of
Nothing -> game -- если поставить фишку нельзя, ничего не изменится
Just newBoard -> game
{ gameBoard = newBoard
, gamePlayer = switchPlayer (gamePlayer game)
, gameWinner = winner newBoard
}
where
place Nothing = Just (Just (gamePlayer game))
place _ = Nothing -- если клетка занята, поставить фишку нельзя
-- | Сменить текущего игрока.
switchPlayer :: Mark -> Mark
switchPlayer X = O
switchPlayer O = X
-- | Применить преобразование к элементу списка
-- с заданным индексом. Если преобразование не удалось — вернуть 'Nothing'.
-- Иначе вернуть преобразованный список.
modifyAt :: Int -> (a -> Maybe a) -> [a] -> Maybe [a]
modifyAt _ _ [] = Nothing
modifyAt 0 f (x:xs) = case f x of
Nothing -> Nothing
Just y -> Just (y : xs)
modifyAt i f (x:xs) = case modifyAt (i - 1) f xs of
Nothing -> Nothing
Just ys -> Just (x : ys)
-- | Получить координаты клетки под мышкой.
mouseToCell :: Point -> (Int, Int)
mouseToCell (x, y) = (i, j)
where
i = floor (x + fromIntegral screenWidth / 2) `div` cellSize
j = floor (y + fromIntegral screenHeight / 2) `div` cellSize
-- | Определить победителя на игровом поле, если такой есть.
winner :: Board -> Maybe Mark
winner board = getFirstWinner (map lineWinner allLines)
where
allLines = rows ++ cols ++ diagonals
rows = board
cols = transpose board
diagonals = lefts board ++ rights board
lefts b = leftTops b ++ leftBottoms b
rights = lefts . reverse
leftTops = transpose . zipWith drop [0..]
leftBottoms = drop 1 . leftTops . transpose
getFirstWinner :: [Maybe a] -> Maybe a
getFirstWinner = foldr first Nothing
where
first Nothing y = y
first x _ = x
lineWinner :: Eq a => [Maybe a] -> Maybe a
lineWinner = winnerSegment . segments
winnerSegment :: [(Maybe a, Int)] -> Maybe a
winnerSegment = foldr compareSegments Nothing
where
compareSegments (Just x, n) _
| n >= winnerStreak = Just x
compareSegments _ y = y
segments :: Eq a => [a] -> [(a, Int)]
segments [] = []
segments (x:xs) = segment : rest
where
segment = (x, 1 + length (takeWhile (== x) xs))
rest = segments (dropWhile (== x) xs)
-- | Оценить состояние игрового поля, а именно
-- вычислить сумму длин сегментов для крестиков и ноликов.
-- Сегменты длины 1 не учитываются при подсчёте.
estimate :: Board -> (Int, Int)
estimate _ = (0, 0)
-- | Обновление игры.
-- В этой игре все изменения происходят только по действиям игрока,
-- поэтому функция обновления — это тождественная функция.
updateGame :: Float -> Game -> Game
updateGame _ = id
-- =========================================
-- Константы, параметры игры
-- =========================================
-- | Ширина игрового поля в клетках.
boardWidth :: Int
boardWidth = 7
-- | Высота игрового поля в клетках.
boardHeight :: Int
boardHeight = 6
-- | Сколько фишек подряд необходимо для выигрыша.
winnerStreak :: Int
winnerStreak = 4
-- | Размер одной клетки в пикселях.
cellSize :: Int
cellSize = 100
-- | Ширина экрана в пикселях.
screenWidth :: Int
screenWidth = cellSize * boardWidth
-- | Высота экрана в пикселях.
screenHeight :: Int
screenHeight = cellSize * boardHeight