First iteration on building a game in ReasonML / Reason-React Check the demo here
Last active
January 7, 2018 22:08
-
-
Save busypeoples/22ae84db570497d2f065aa2aaabf3d22 to your computer and use it in GitHub Desktop.
ReasonML Game
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type field = | |
| O | |
| X; | |
type position = (field, field, field, field); | |
type unit = (position, position, position, position); | |
type units = list(unit); | |
let unit1: unit = ((O, O, X, O), (O, O, X, O), (O, O, X, O), (O, O, X, O)); | |
let unit2: unit = ((O, O, X, O), (O, X, X, O), (O, O, X, O), (O, O, O, O)); | |
let unit3: unit = ((O, O, O, O), (O, X, X, O), (O, X, X, O), (O, O, O, O)); | |
let unit4: unit = ((O, X, O, O), (O, X, O, O), (O, X, X, O), (O, O, O, O)); | |
let unit5: unit = ((O, O, O, O), (O, O, X, X), (O, X, X, O), (O, O, O, O)); | |
let unit6: unit = ((O, O, X, O), (O, O, X, O), (O, X, X, O), (O, O, O, O)); | |
let unit7: unit = ((O, O, O, O), (O, X, X, O), (O, O, X, X), (O, O, O, O)); | |
let units: units = [unit1, unit2, unit3, unit4, unit5, unit6, unit7]; | |
/* | |
constants | |
*/ | |
let numberOfRows = 20; | |
let numberOfColumns = 10; | |
let msTimeout = 300; | |
/* | |
Helper functions | |
*/ | |
let rotateLeft = (unit: unit) : unit => { | |
let ((a1, a2, a3, a4), (b1, b2, b3, b4), (c1, c2, c3, c4), (d1, d2, d3, d4)) = unit; | |
((a4, b4, c4, d4), (a3, b3, c3, d3), (a2, b2, c2, d2), (a1, b1, c1, d1)) | |
}; | |
let rotateRight = (unit: unit) : unit => { | |
let ((a1, a2, a3, a4), (b1, b2, b3, b4), (c1, c2, c3, c4), (d1, d2, d3, d4)) = unit; | |
((d1, c1, b1, a1), (d2, c2, b2, a2), (d3, c3, b3, a3), (d4, c4, b4, a4)) | |
}; | |
let unitToArray = (unit: unit) : array(array(field)) => { | |
let ((a1, a2, a3, a4), (b1, b2, b3, b4), (c1, c2, c3, c4), (d1, d2, d3, d4)) = unit; | |
[| | |
[|a1, a2, a3, a4|], | |
[|b1, b2, b3, b4|], | |
[|c1, c2, c3, c4|], | |
[|d1, d2, d3, d4|] | |
|] | |
}; | |
let isIntersecting = | |
(rows: array(array(field)), unit: unit, y: int, x: int) | |
: bool => { | |
let unitArr = unitToArray(unit); | |
let found = ref(false); | |
Array.iteri( | |
(i, row) => | |
Array.iteri( | |
(j, col) => | |
switch col { | |
| X => | |
if (y | |
+ i >= numberOfRows | |
|| x | |
+ j < 0 | |
|| x | |
+ j >= numberOfColumns | |
|| rows[y + i][x + j] === X) { | |
found := true | |
} | |
| _ => () | |
}, | |
row | |
), | |
unitArr | |
); | |
found^ | |
}; | |
let createRows = () => Array.make_matrix(numberOfRows, numberOfColumns, O); | |
let updateBoard = | |
(rows: array(array(field)), unit: unit, x: int, y: int) | |
: array(array(field)) => { | |
let newRows = Array.map((row) => Array.copy(row), rows); | |
let unitArr = unitToArray(unit); | |
Array.iteri( | |
(i, row) => | |
Array.iteri( | |
(j, col) => | |
switch col { | |
| X => newRows[y + i][x + j] = X | |
| _ => () | |
}, | |
row | |
), | |
unitArr | |
); | |
newRows | |
}; | |
let removeFinishedRows = (board: array(array(field))) : array(array(field)) => | |
Array.fold_left( | |
(result, row) => | |
switch row { | |
| [|X, X, X, X, X, X, X, X, X, X|] => result | |
| _ => Array.append(result, [|Array.copy(row)|]) | |
}, | |
[||], | |
board | |
); | |
let randomizeUnit = (units: units) : unit => | |
List.nth(units, Random.int(List.length(units))); | |
let se = ReasonReact.stringToElement; | |
let getBackgroundColor = (col) => | |
switch col { | |
| X => "#000" | |
| _ => "#fff" | |
}; | |
type gameState = | |
| Initial | |
| Play | |
| Pause | |
| End; | |
/* | |
<Board /> | |
*/ | |
module Board = { | |
let component = ReasonReact.statelessComponent("Board"); | |
let make = (~rows, ~gameState=Initial, _children) => { | |
...component, | |
render: (_self) => | |
<div> | |
<div | |
style=( | |
ReactDOMRe.Style.make( | |
~opacity= | |
switch gameState { | |
| End => ".5" | |
| _ => "1" | |
}, | |
() | |
) | |
)> | |
( | |
rows | |
|> Array.mapi( | |
(i, row) => | |
<div | |
key=("key" ++ string_of_int(i)) | |
style=(ReactDOMRe.Style.make(~display="inline", ()))> | |
( | |
row | |
|> Array.mapi( | |
(j, col) => | |
<div | |
key=( | |
"col" | |
++ string_of_int(i) | |
++ "-" | |
++ string_of_int(j) | |
) | |
style=( | |
ReactDOMRe.Style.make( | |
~width="30px", | |
~height="30px", | |
~float="left", | |
~border="1px solid #eee", | |
~background=getBackgroundColor(col), | |
() | |
) | |
) | |
/> | |
) | |
|> ReasonReact.arrayToElement | |
) | |
<br style=(ReactDOMRe.Style.make(~clear="both", ())) /> | |
</div> | |
) | |
|> ReasonReact.arrayToElement | |
) | |
</div> | |
</div> | |
}; | |
}; | |
/* | |
<Info /> | |
*/ | |
module Info = { | |
let component = ReasonReact.statelessComponent("Info"); | |
let make = (~score, ~next, ~gameState, _children) => { | |
...component, | |
render: (_self) => | |
<div> | |
<h3> (se("ReasonML Experiment")) </h3> | |
<br /> | |
<div> | |
( | |
switch gameState { | |
| Initial => se("Press the spacebar to start") | |
| Play => se("Press the spacebar to Pause") | |
| Pause => se("Press the spacebar to Continue") | |
| End => se("Game Over! Press the spacebar to Restart") | |
} | |
) | |
</div> | |
<br /> | |
<div> <h4> (se("Score: " ++ string_of_int(score))) </h4> </div> | |
<br /> | |
<div> (se("Next:")) <br /> <br /> <Board rows=next /> </div> | |
<br /> | |
<div> | |
(se("How to Play:")) | |
<br /> | |
<br /> | |
<div> (se("a/d: rotate (left/right)")) </div> | |
<div> (se("j/k: navigate (left/right)")) </div> | |
<div> (se("s: navigate (down)")) </div> | |
</div> | |
</div> | |
}; | |
}; | |
/* | |
Game | |
*/ | |
type actions = | |
| Toggle | |
| Tick | |
| MoveLeft | |
| MoveRight | |
| MoveDown | |
| RotateLeft | |
| RotateRight | |
| Drop; | |
type state = { | |
gameState, | |
score: int, | |
board: array(array(field)), | |
unit, | |
next: unit, | |
posX: int, | |
posY: int | |
}; | |
let getKey = (event, reduce) => { | |
let key = ReactEventRe.Keyboard.charCode(event); | |
switch key { | |
| 32 => reduce(() => Toggle, ()) | |
| 97 => reduce(() => RotateLeft, ()) | |
| 100 => reduce(() => RotateRight, ()) | |
| 106 => reduce(() => MoveLeft, ()) | |
| 107 => reduce(() => MoveRight, ()) | |
| 115 => reduce(() => MoveDown, ()) | |
| _ => () | |
} | |
}; | |
let intervalId = ref(None); | |
module Game = { | |
let component = ReasonReact.reducerComponent("Game"); | |
let initializeState = () => { | |
gameState: Initial, | |
score: 0, | |
unit: randomizeUnit(units), | |
next: randomizeUnit(units), | |
posY: 0, | |
posX: 3, | |
board: Array.copy(createRows()) | |
}; | |
let play = (reduce, ()) => { | |
reduce((_) => Tick, ()); | |
() | |
}; | |
let make = (_children) => { | |
...component, | |
initialState: initializeState, | |
reducer: (action, state) => | |
switch action { | |
| Toggle => | |
switch state.gameState { | |
| Play => | |
ReasonReact.UpdateWithSideEffects( | |
{...state, gameState: Pause}, | |
( | |
(_self) => { | |
switch intervalId^ { | |
| Some(id) => | |
Js.Global.clearInterval(id); | |
intervalId := None | |
| None => () | |
}; | |
() | |
} | |
) | |
) | |
| End => | |
ReasonReact.UpdateWithSideEffects( | |
{...initializeState(), gameState: Play}, | |
( | |
(self) => { | |
switch intervalId^ { | |
| Some(_id) => () | |
| None => | |
intervalId := | |
Some(Js.Global.setInterval(play(self.reduce), msTimeout)) | |
}; | |
() | |
} | |
) | |
) | |
| _ => | |
ReasonReact.UpdateWithSideEffects( | |
{...state, gameState: Play}, | |
( | |
(self) => { | |
switch intervalId^ { | |
| Some(_id) => () | |
| None => | |
intervalId := | |
Some(Js.Global.setInterval(play(self.reduce), msTimeout)) | |
}; | |
() | |
} | |
) | |
) | |
} | |
| Tick => | |
if (state.gameState === End) { | |
switch intervalId^ { | |
| Some(id) => | |
Js.Global.clearInterval(id); | |
intervalId := None | |
| None => () | |
}; | |
ReasonReact.NoUpdate | |
} else if (isIntersecting( | |
state.board, | |
state.unit, | |
state.posY + 1, | |
state.posX | |
)) { | |
let nextBoard = | |
updateBoard(state.board, state.unit, state.posX, state.posY); | |
let nextRows = removeFinishedRows(nextBoard); | |
let rowsRemoved = numberOfRows - Array.length(nextRows); | |
let newRows = Array.make_matrix(rowsRemoved, 10, O); | |
let board = Array.append(newRows, nextRows); | |
let score = | |
state.score + 10 + rowsRemoved * rowsRemoved * numberOfColumns * 3; | |
let unit = state.next; | |
let posY = 0; | |
let posX = numberOfColumns / 2 - 2; | |
let next = randomizeUnit(units); | |
if (isIntersecting(board, state.next, 0, numberOfColumns / 2 - 2)) { | |
ReasonReact.Update({ | |
unit, | |
posY, | |
posX, | |
next, | |
score, | |
board, | |
gameState: End | |
}) | |
} else { | |
ReasonReact.Update({...state, unit, posY, posX, next, score, board}) | |
} | |
} else { | |
ReasonReact.Update({...state, posY: state.posY + 1}) | |
} | |
| MoveLeft => | |
if (state.gameState === Play | |
&& ! | |
isIntersecting( | |
state.board, | |
state.unit, | |
state.posY, | |
state.posX - 1 | |
)) { | |
ReasonReact.Update({...state, posX: state.posX - 1}) | |
} else { | |
ReasonReact.NoUpdate | |
} | |
| MoveRight => | |
if (state.gameState === Play | |
&& ! | |
isIntersecting( | |
state.board, | |
state.unit, | |
state.posY, | |
state.posX + 1 | |
)) { | |
ReasonReact.Update({...state, posX: state.posX + 1}) | |
} else { | |
ReasonReact.NoUpdate | |
} | |
| MoveDown => | |
if (state.gameState === Play | |
&& ! | |
isIntersecting( | |
state.board, | |
state.unit, | |
state.posY + 1, | |
state.posX | |
)) { | |
ReasonReact.Update({...state, posY: state.posY + 1}) | |
} else { | |
ReasonReact.NoUpdate | |
} | |
| RotateLeft => | |
let newPiece = rotateLeft(state.unit); | |
if (state.gameState === Play | |
&& ! isIntersecting(state.board, newPiece, state.posY, state.posX)) { | |
ReasonReact.Update({...state, unit: newPiece}) | |
} else { | |
ReasonReact.NoUpdate | |
} | |
| RotateRight => | |
let newPiece = rotateRight(state.unit); | |
if (state.gameState === Play | |
&& ! isIntersecting(state.board, newPiece, state.posY, state.posX)) { | |
ReasonReact.Update({...state, unit: newPiece}) | |
} else { | |
ReasonReact.NoUpdate | |
} | |
| Drop => ReasonReact.NoUpdate /* Implement later on */ | |
}, | |
render: ({state, reduce}) => { | |
let {board, unit, posY, posX, score, gameState, next} = state; | |
let displayRows = | |
switch gameState { | |
| Initial => board | |
| _ => updateBoard(board, unit, posX, posY) | |
}; | |
let divStyleLeft = | |
ReactDOMRe.Style.make( | |
~width="30%", | |
~float="left", | |
~padding="3%", | |
~minWidth="450px", | |
() | |
); | |
let divStyleRight = | |
ReactDOMRe.Style.make( | |
~float="left", | |
~paddingLeft="3%", | |
~paddingRight="3%", | |
~paddingTop="1%", | |
~paddingBottom="5%", | |
() | |
); | |
let mainStyling = | |
ReactDOMRe.Style.make( | |
~outlineColor="#fff", | |
~fontSize="1.5em", | |
~width="90%", | |
() | |
); | |
<div onKeyPress=((e) => getKey(e, reduce)) style=mainStyling tabIndex=0> | |
(se ("Click anywhere on the screen for focus")) | |
<div style=divStyleLeft> <Board rows=displayRows gameState /> </div> | |
<div style=divStyleRight> | |
<Info score next=(unitToArray(next)) gameState /> | |
</div> | |
</div> | |
} | |
}; | |
}; | |
ReactDOMRe.renderToElementWithId(<Game />, "root"); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment