Skip to content

Instantly share code, notes, and snippets.

@hakelimopu
Last active November 7, 2015 15:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hakelimopu/2581ff54bfcb886d7507 to your computer and use it in GitHub Desktop.
Save hakelimopu/2581ff54bfcb886d7507 to your computer and use it in GitHub Desktop.
C# and F# versions of the same game
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Reflection;
using System.Windows.Forms;
namespace csharpversion
{
class Program
{
static readonly Size screenSize = new Size(640,480);
static readonly Size boardSize = new Size(40, 30);
static readonly Size cellSize = new Size(screenSize.Width / boardSize.Width, screenSize.Height / boardSize.Height);
const int tailLength = 6;
enum GameState
{
Title,
Play
}
enum Direction
{
Left,
Straight,
Right
}
static int directionDelta(Direction direction)
{
switch (direction)
{
case Direction.Left:
return -1;
case Direction.Right:
return 1;
default:
return 0;
}
}
static Queue<int> createIntList(int count, int value)
{
Queue<int> result = new Queue<int>();
while (result.Count() < count)
{
result.Enqueue(value);
}
return result;
}
static void drawBricks(Graphics g, Brush brush, int row, Queue<int> bricks)
{
foreach (int head in bricks)
{
g.FillRectangle(brush, new Rectangle(cellSize.Width * head, cellSize.Height * row, cellSize.Width, cellSize.Height));
row++;
}
}
static void drawTail(Graphics g, Brush tailBrush, Brush headBrush, int row, Queue<int> tail)
{
foreach (int head in tail)
{
Brush brush = tailBrush;
if (row==tailLength-1)
{
brush = headBrush;
}
g.FillRectangle(brush, new Rectangle(cellSize.Width * head, cellSize.Height * row, cellSize.Width, cellSize.Height));
row++;
}
}
static Form gameWindow = new Form();
static void scroll(int value, Queue<int> items)
{
items.Enqueue(value);
items.Dequeue();
}
static int calculateScore(int runSize)
{
return (runSize * (runSize + 1)) / 2;
}
class GameData
{
GameState gameState = GameState.Title;
int score = 0;
Queue<int> blocks = createIntList(boardSize.Height,0);
Queue<int> tail = createIntList(tailLength, boardSize.Width / 2);
int currentRun = 0;
Direction direction = Direction.Straight;
static readonly Brush wallBrush = new SolidBrush(Color.Blue);
static readonly Brush tailBrush = new SolidBrush(Color.Orange);
static readonly Brush headBrush = new SolidBrush(Color.Red);
static readonly Brush blockBrush = new SolidBrush(Color.White);
static readonly Brush fontBrush = new SolidBrush(Color.LightGreen);
static readonly Rectangle leftWall = new Rectangle(0, 0, cellSize.Width, screenSize.Height);
static readonly Rectangle rightWall = new Rectangle(screenSize.Width - cellSize.Width,0,cellSize.Width,screenSize.Height);
static readonly Font font = new Font("Tahoma", cellSize.Height );
static readonly Random random = new Random();
const string gameOverText = "Press Space";
public void HandleKey(Keys keyCode)
{
switch (gameState)
{
case GameState.Title:
if (keyCode == Keys.Space)
{
Reset();
gameState = GameState.Play;
}
break;
default:
switch (keyCode)
{
case Keys.Left:
if (direction == Direction.Right)
{
score += calculateScore(currentRun);
}
direction = Direction.Left;
currentRun = 0;
break;
case Keys.Right:
if (direction == Direction.Left)
{
score += calculateScore(currentRun);
}
direction = Direction.Right;
currentRun = 0;
break;
}
break;
}
}
public void HandlePaint(Graphics g)
{
g.Clear(Color.Black);
drawBricks(g, blockBrush, 0, blocks);
g.FillRectangle(wallBrush, leftWall);
g.FillRectangle(wallBrush,rightWall);
drawTail(g,tailBrush,headBrush,0, tail);
g.DrawString(score.ToString(),font,fontBrush,new PointF(cellSize.Width , 0.0f));
if (gameState == GameState.Title)
{
var gameOverSize = g.MeasureString(gameOverText, font);
g.DrawString(gameOverText, font, fontBrush, new PointF(screenSize.Width / 2.0f - gameOverSize.Width / 2.0f, screenSize.Height / 2.0f - gameOverSize.Height / 2.0f));
}
}
public void HandleTick()
{
if (gameState == GameState.Play)
{
int nextBlockColumn = random.Next(boardSize.Width-2)+1;
scroll(nextBlockColumn,blocks);
int nextTailColumn = tail.Last() + directionDelta(direction);
scroll(nextTailColumn, tail);
currentRun++;
gameWindow.Invalidate();
if (tail.Last() == 0 || tail.Last() == boardSize.Width - 1 || tail.Last() == blocks.ToList()[tailLength - 1])
{
gameState = GameState.Title;
}
}
}
public void Reset()
{
blocks = createIntList(boardSize.Height, 0);
tail = createIntList(tailLength, boardSize.Width / 2);
score = 0;
gameState = GameState.Title;
direction = Direction.Straight;
gameWindow.Invalidate();
}
}
static GameData gameData = new GameData();
static Timer timer = new Timer();
[STAThread]
static void Main()
{
gameWindow.ClientSize = screenSize;
gameWindow.FormBorderStyle = FormBorderStyle.FixedSingle;
gameWindow.MaximizeBox = false;
gameWindow.MinimizeBox = false;
gameWindow.GetType().GetProperty("DoubleBuffered", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(gameWindow,true,null);
gameWindow.Text = "C# JetLag";
gameWindow.KeyDown+=(_, e) => gameData.HandleKey(e.KeyCode);
gameWindow.Paint+=(_, g) => gameData.HandlePaint(g.Graphics);
timer.Interval = 100;
timer.Tick+=(_1, _2) => gameData.HandleTick();
timer.Start();
Application.Run(gameWindow);
}
}
}
open System
open System.Windows.Forms
open System.Drawing
open System.Reflection
(*
I like the simplicity of these declarations.
In C#, these needed to be static readonly, but that's what 'let' means on the module scope to begin with, apparently.
*)
let screenSize = new Size(640, 480)
let boardSize = new Size(40, 30)
let cellSize = new Size(screenSize.Width / boardSize.Width, screenSize.Height / boardSize.Height)
let tailLength = 6
(*
Yeah, so I'm using unions like enums.
But the F# enum requires extra text
So, these are discount enums
*)
type GameState = Title | Play
type Direction = Left | Straight | Right
(*
Apparently this is how lookup tables are to be done.
*)
let directionDelta (direction: Direction) =
(*
for some reason, I have to do it this way, with a match, instead of the function way I've seen elsewhere, because of my direction |> directionDelta
which means I don't understand the keyword function or I don't fully understand |>
*)
match direction with
| Left -> -1
| Right -> 1
| _ -> 0
(*
I made a decision to use int list for my blocks and tail
In C#, it made more sense in a Queue<int>
After doing that, it makes more sense for them to be Queue<int> in F# as well
But if I do that, what sort of F#-ness am I really using?
*)
let rec createIntList (count:int) (value:int) (current: int list)=
match count with
| x when x <= 0 -> current
| _ -> value :: current |> createIntList (count-1) (value)
(*
As a result of the int list, I have to draw from bottom to top so that adding a new line is just a :: operator call
Of course, that doesn't really save me anything for when scrolling and taking the
Or, I could have just left the entire list of blocks and tail.
*)
let rec drawBricks (g:Graphics) (brush: Brush) (row: int) (bricks: int list) =
match bricks with
| [] -> ()
| head :: tail ->
g.FillRectangle(brush, new Rectangle(cellSize.Width * head, cellSize.Height * row, cellSize.Width, cellSize.Height))
drawBricks g brush (row-1) tail
(*
tail has to be drawn backwards as well.
if I had it to do over, I'd leave the tail length check as an if
*)
let rec drawTail (g:Graphics) (tailBrush: Brush) (headBrush:Brush) (row: int) (tail:int list) =
match tail with
| [] -> ()
| head :: tail ->
let brush =
match tail.Length with
| x when x = (tailLength - 1) -> headBrush
| _ -> tailBrush
g.FillRectangle(brush, new Rectangle(cellSize.Width * head, cellSize.Height * row, cellSize.Width, cellSize.Height))
drawTail g tailBrush headBrush (row-1) tail
(*
This was a nice, easy way to create the form. C# doesn't quite have this equivalent.
*)
let gameWindow = new Form(ClientSize=screenSize, FormBorderStyle=FormBorderStyle.FixedSingle, MaximizeBox=false, MinimizeBox=false)
(*
the conversion to sequence and then back to list unsettles me
*)
let scroll (value:int) (items: int list)=
value :: (items |> Seq.take (items.Length-1) |> List.ofSeq)
(*
could have used recursion here, but it would have been recursion for the sake of recursion
*)
let calculateScore runSize = ( (runSize) * (runSize + 1) ) / 2
(*
this is my first f# class
*)
type GameData()=
//I know that mutable is to be avoided, but these are variables
let mutable gameState = Title
let mutable score = 0
let mutable blocks = createIntList boardSize.Height 0 []
let mutable tail = createIntList tailLength (boardSize.Width / 2) []
let mutable currentRun = 0
let mutable direction = Straight
//immutables
let wallBrush = new SolidBrush(Color.Blue)
let tailBrush = new SolidBrush(Color.Orange)
let headBrush = new SolidBrush(Color.Red)
let blockBrush = new SolidBrush(Color.White)
let leftWall = new Rectangle(0,0,cellSize.Width,screenSize.Height)
let rightWall = new Rectangle(screenSize.Width - cellSize.Width,0,cellSize.Width,screenSize.Height)
let font = new Font("Tahoma", float32 cellSize.Height )
let fontBrush = new SolidBrush(Color.LightGreen)
let random = new Random()
let gameOverText = "Press Space"
//I think if i had it to do over, I'd have used a tuple with keyCode, gameState, and direction in it, and matched on that
member this.HandleKey (keyCode: Keys) =
match gameState with
| Title ->
match keyCode with
| Keys.Space ->
this.Reset()
gameState <- Play
| _ -> ()
| _ ->
match keyCode with
| Keys.Left ->
match direction with
| Right ->
score <- score + (currentRun |> calculateScore)
currentRun <- 0
direction <- Left
| Straight ->
currentRun <- 0
direction <- Left
| _ -> ()
| Keys.Right ->
match direction with
| Left ->
score <- score + (currentRun |> calculateScore)
currentRun <- 0
direction <- Right
| Straight ->
currentRun <- 0
direction <- Right
| _ -> ()
| _ -> ()
(*
the match in here is gratuitous, and should be an if
the use of |> is similarly gratuitous
i have dubbed |> the "leapfrog operator"
*)
member this.HandlePaint (g:Graphics) =
g.Clear Color.Black
blocks |> drawBricks g blockBrush (boardSize.Height-1)
g.FillRectangle(wallBrush,leftWall)
g.FillRectangle(wallBrush,rightWall)
tail |> drawTail g tailBrush headBrush (tailLength-1)
g.DrawString(score.ToString(),font,fontBrush,new PointF(float32 cellSize.Width , 0.0f))
match gameState with
| Title ->
let gameOverSize = g.MeasureString(gameOverText, font)
g.DrawString(gameOverText, font, fontBrush, new PointF(float32 screenSize.Width/2.0f - gameOverSize.Width/2.0f,float32 screenSize.Height/2.0f - gameOverSize.Height/2.0f))
| _ -> ()
(*
so, the <- was an early (before this project) stumbling block for me
gratuitous use of |>
*)
member this.Reset () =
blocks <- [] |> createIntList boardSize.Height 0
tail <- [] |> createIntList tailLength (boardSize.Width / 2)
score <- 0
gameState <- Title
direction <- Straight
gameWindow.Invalidate()
//matches should be if
member this.HandleTick () =
match gameState with
| Play ->
let nextBlockColumn = random.Next(boardSize.Width-2)+1
blocks <- blocks |> scroll nextBlockColumn
let nextTailColumn = tail.Head + (direction |> directionDelta)
tail <- tail |> scroll nextTailColumn
currentRun <- currentRun + 1
gameWindow.Invalidate()
match tail.Head with
| x when x=0 || x=boardSize.Width-1 || x = List.nth blocks (boardSize.Height - tailLength) ->
gameState <- Title
| _ -> ()
| _ -> ()
let gameData = new GameData()
//its nice that I can call methods on things outside of the main function
gameData.Reset()
(*
the following line was a big struggle.
because of |||
I was not expecting the |||
its main weapons are fear and surprise
it makes some level of sense based on how | is used in F#
but it made me do a wtf
*)
gameWindow.GetType().GetProperty("DoubleBuffered", BindingFlags.Instance ||| BindingFlags.NonPublic).SetValue(gameWindow,true,null)
gameWindow.Text <- "F# JetLag - Why? ::shrug::"
(*
oddly enough, adding the handlers was more verbose than the C# equivalent
*)
gameWindow.KeyDown.AddHandler( fun _ e -> gameData.HandleKey e.KeyCode )
gameWindow.Paint.AddHandler(fun _ g -> gameData.HandlePaint g.Graphics )
(*
good ol Timer.
not very accurate, but he does the job
*)
let timer = new Timer(Interval = 100)
timer.Tick.AddHandler(fun _ _ -> gameData.HandleTick())
timer.Start()
[<EntryPoint>]
[<STAThread>]
let main argv =
Application.Run(gameWindow)
0
@hakelimopu
Copy link
Author

Instructions: Space to start game. Left/Right to steer.

@isaacabraham
Copy link

F# 4 has a List.take function AFAIK.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment