Skip to content

Instantly share code, notes, and snippets.

@busypeoples
Last active January 7, 2018 22:08
Show Gist options
  • Save busypeoples/22ae84db570497d2f065aa2aaabf3d22 to your computer and use it in GitHub Desktop.
Save busypeoples/22ae84db570497d2f065aa2aaabf3d22 to your computer and use it in GitHub Desktop.
ReasonML Game

First iteration on building a game in ReasonML / Reason-React Check the demo here

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