问题
Edit: So i followed your pointers and got to this point: (following the rules of play)
https://hackage.haskell.org/package/gloss-1.9.4.1/docs/Graphics-Gloss-Interface-Pure-Game.html
drawBoard :: IO ()
drawBoard = play (InWindow "Tic Tac Toe" (300,300)(10,10)) yellow 10 board (testBoard) (handleKeys) (iteration)
testBoard :: Board -> Picture
testBoard board = grid
where
grid =
color black (line [ (-100, -300), (-100, 300) ])
iteration :: Float -> Board -> Board
iteration _ board = board
handleKeys :: Event -> Board -> Board
handleKeys (EventKey (MouseButton LeftButton) Up _ (x, y)) board = board
handleKeys _ _ = board
board = []
And it correctly opens the window and draws a black line accoarding to my testBoard
function.
What im not sure about now is how to pass a new board when I click a button. Do I create another function to draw the new board or how would I go on about drawing a new board when clicking?
回答1:
You want to use play instead of display
.
It explicitly takes an argument of type Event -> world -> world
(world
would be Board
in your case) describing how to react to Event
s.
Edit: Ok, I have implemented a toy example to show you how I'd structure my code. You can find it in a self-contained gist with the appropriate imports.
What is the state of the system? How does it evolve?
The first thing you need to figure out is how to represent the state of the board. In a game of Tic-tac-toe, you have a bunch of noughts and a bunch of crosses at given coordinates (a pair of Int
s equal to either -1, 0 or 1) on the board. You also have a current player (which will change after the next move). So let's start with that:
type Coordinates = (Int, Int)
data Player = Nought | Cross
data Board = Board
{ noughts :: [Coordinates]
, crosses :: [Coordinates]
, player :: Player
}
You can then describe various simple things. What should the empty board look like when you're beginning a match? What does a player's move (putting a new token on the board) do to the state of the system (it inserts the token in the list of the current player and then changes the current player to the adversary):
emptyBoard :: Board
emptyBoard = Board [] [] Nought
pushToken:: Coordinates -> Board -> Board
pushToken c b = case player b of
Nought -> b { noughts = c : noughts b, player = Cross }
Cross -> b { crosses = c : crosses b, player = Nought }
How do we display the state to the user?
Next comes the problem of drawing a Picture
corresponding to the current state. Here, I assume that the picture will be centered in (0, 0)
which means that changing its size can be done by simply multiplying all the coordinates by a given constant. I will parameterise all my functions with a Size
argument allowing me to tweak the size of the displayed board without any hassle.
type Size = Float
resize :: Size -> Path -> Path
resize k = fmap (\ (x, y) -> (x * k, y * k))
Noughts are easy to display: we can simply use a thickCircle
of the right size and then translate them to their coordinate! Crosses are a bit harder to deal with because you have to combine 2 rectangles to draw them.
drawNought :: Size -> Coordinates -> Picture
drawNought k (x, y) =
let x' = k * fromIntegral x
y' = k * fromIntegral y
in color green $ translate x' y' $ thickCircle (0.1 * k) (0.3 * k)
drawCross :: Size -> Coordinates -> Picture
drawCross k (x, y) =
let x' = k * fromIntegral x
y' = k * fromIntegral y
in color red $ translate x' y' $ Pictures
$ fmap (polygon . resize k)
[ [ (-0.35, -0.25), (-0.25, -0.35), (0.35,0.25), (0.25, 0.35) ]
, [ (0.35, -0.25), (0.25, -0.35), (-0.35,0.25), (-0.25, 0.35) ]
]
In order to draw the board, we draw a black grid and then populate it with the noughts and crosses:
drawBoard :: Size -> Board -> Picture
drawBoard k b = Pictures $ grid : ns ++ cs where
ns = fmap (drawNought k) $ noughts b
cs = fmap (drawCross k) $ crosses b
grid :: Picture
grid = color black $ Pictures $ fmap (line . resize k)
[ [(-1.5, -0.5), (1.5 , -0.5)]
, [(-1.5, 0.5) , (1.5 , 0.5)]
, [(-0.5, -1.5), (-0.5, 1.5)]
, [(0.5 , -1.5), (0.5 , 1.5)]
]
How do we react to inputs?
Now that we have a board and that we can display it, we just have to be able to grab user inputs and respond to them so that we have a working game.
Mouse clicks are received as a pair of floats corresponding to the position of the mouse in the drawing. We need to translate this position into appropriate coordinates. This is what checkCoordinate
does: it divides a Float
by the size we have picked for the drawing and checks which subdivision of the board that position corresponds to.
Here I use guard, (<$) and (<|>) to have a declarative presentation of the various cases but you could use if ... then ... else ...
if you wanted to.
checkCoordinate :: Size -> Float -> Maybe Int
checkCoordinate k f' =
let f = f' / k
in (-1) <$ guard (-1.5 < f && f < -0.5)
<|> 0 <$ guard (-0.5 < f && f < 0.5)
<|> 1 <$ guard (0.5 < f && f < 1.5)
Finally, handleKeys
can detect mouse clicks, check that they correspond to a position in the board and react appropriately by calling pushToken
:
handleKeys :: Size -> Event -> Board -> Board
handleKeys k (EventKey (MouseButton LeftButton) Down _ (x', y')) b =
fromMaybe b $ do
x <- checkCoordinate k x'
y <- checkCoordinate k y'
return $ pushToken (x, y) b
handleKeys k _ b = b
Putting it all together
We can then declare a main
function creating the window, starting the game with an emptyBoard
, displaying it using drawBoard
and handling user inputs with handleKeys
.
main :: IO ()
main =
let window = InWindow "Tic Tac Toe" (300, 300) (10, 10)
size = 100.0
in play window yellow 1 emptyBoard (drawBoard size) (handleKeys size) (flip const)
What is there left to do?
I haven't enforced any of the game logic:
player can put a token in a subdivision of the grid that's already occupied,
the game does not detect when there is a winner
there is no way to play multiple games in a row
来源:https://stackoverflow.com/questions/35507997/reading-user-mouseclick-position-in-haskells-gloss