【问题标题】:Very simple RogueLike in F#, making it more "functional"F# 中非常简单的 RogueLike,使其更具“功能性”
【发布时间】:2011-05-28 14:46:12
【问题描述】:

我有一些现有的 C# 代码,用于非常非常简单的 RogueLike 引擎。故意天真,因为我试图尽可能简单地做最少的数量。它所做的只是使用箭头键和 System.Console 在硬编码地图周围移动一个 @ 符号:

//define the map
var map = new List<string>{
  "                                        ",
  "                                        ",
  "                                        ",
  "                                        ",
  "    ###############################     ",
  "    #                             #     ",
  "    #         ######              #     ",
  "    #         #    #              #     ",
  "    #### #### #    #              #     ",
  "       # #  # #    #              #     ",
  "       # #  # #    #              #     ",
  "    #### #### ######              #     ",
  "    #              =              #     ",
  "    #              =              #     ",
  "    ###############################     ",
  "                                        ",
  "                                        ",
  "                                        ",
  "                                        ",
  "                                        "
};

//set initial player position on the map
var playerX = 8;
var playerY = 6;

//clear the console
Console.Clear();

//send each row of the map to the Console
map.ForEach( Console.WriteLine );

//create an empty ConsoleKeyInfo for storing the last key pressed
var keyInfo = new ConsoleKeyInfo( );

//keep processing key presses until the player wants to quit
while ( keyInfo.Key != ConsoleKey.Q ) {
  //store the player's current location
  var oldX = playerX;
  var oldY = playerY;

  //change the player's location if they pressed an arrow key
  switch ( keyInfo.Key ) {
    case ConsoleKey.UpArrow:
      playerY--;
      break;
    case ConsoleKey.DownArrow:
      playerY++;
      break;
    case ConsoleKey.LeftArrow:
      playerX--;
      break;
    case ConsoleKey.RightArrow:
      playerX++;
      break;
  }

  //check if the square that the player is trying to move to is empty
  if( map[ playerY ][ playerX ] == ' ' ) {
    //ok it was empty, clear the square they were standing on before
    Console.SetCursorPosition( oldX, oldY );
    Console.Write( ' ' );
    //now draw them at the new square
    Console.SetCursorPosition( playerX, playerY );
    Console.Write( '@' );
  } else {
    //they can't move there, change their location back to the old location
    playerX = oldX;
    playerY = oldY;
  }

  //wait for them to press a key and store it in keyInfo
  keyInfo = Console.ReadKey( true );
}

我在 F# 中玩弄它,最初我试图使用函数概念来编写它,但结果我有点过头了,所以我做了一个直接的移植 - 它不是真的 一个 F# 程序(尽管它可以编译和运行)它是一个用 F# 语法编写的过程程序:

open System

//define the map
let map = [ "                                        ";
            "                                        ";
            "                                        ";
            "                                        ";
            "    ###############################     ";
            "    #                             #     ";
            "    #         ######              #     ";
            "    #         #    #              #     ";
            "    #### #### #    #              #     ";
            "       # #  # #    #              #     ";
            "       # #  # #    #              #     ";
            "    #### #### ######              #     ";
            "    #              =              #     ";
            "    #              =              #     ";
            "    ###############################     ";
            "                                        ";
            "                                        ";
            "                                        ";
            "                                        ";
            "                                        " ]

//set initial player position on the map
let mutable playerX = 8
let mutable playerY = 6

//clear the console
Console.Clear()

//send each row of the map to the Console
map |> Seq.iter (printfn "%s")

//create an empty ConsoleKeyInfo for storing the last key pressed
let mutable keyInfo = ConsoleKeyInfo()

//keep processing key presses until the player wants to quit
while not ( keyInfo.Key = ConsoleKey.Q ) do
    //store the player's current location
    let mutable oldX = playerX
    let mutable oldY = playerY

    //change the player's location if they pressed an arrow key
    if keyInfo.Key = ConsoleKey.UpArrow then
        playerY <- playerY - 1
    else if keyInfo.Key = ConsoleKey.DownArrow then
        playerY <- playerY + 1
    else if keyInfo.Key = ConsoleKey.LeftArrow then
        playerX <- playerX - 1
    else if keyInfo.Key = ConsoleKey.RightArrow then
        playerX <- playerX + 1

    //check if the square that the player is trying to move to is empty
    if map.Item( playerY ).Chars( playerX ) = ' ' then
        //ok it was empty, clear the square they were standing on
        Console.SetCursorPosition( oldX, oldY )
        Console.Write( ' ' )
        //now draw them at the new square 
        Console.SetCursorPosition( playerX, playerY )
        Console.Write( '@' )
    else
        //they can't move there, change their location back to the old location
        playerX <- oldX
        playerY <- oldY

    //wait for them to press a key and store it in keyInfo
    keyInfo <- Console.ReadKey( true )

所以我的问题是,为了更实用地重写它,我需要学习什么,你能给我一些提示,一个模糊的概述之类的东西。

我宁愿向正确的方向推进,而不是仅仅看到一些代码,但如果这是你向我解释它的最简单方法,那么很好,但在这种情况下,你能否也解释一下“为什么”而不是它的“如何”?

【问题讨论】:

  • 这不是开始使用 F# 或一般函数式编程的最简单任务。状态是 @ 符号定位的固有属性,我认为以适当的函数形式执行此操作需要 monad 或其他箭头构造。
  • @jon_darkstar:完全有可能在不使用任何类似结构的情况下实现它。事实上,我建议您先自己实现该模式,以便在将其抽象到 monad 后面之前了解它在做什么。
  • 是的,我想我有点想多了。现在我仔细阅读,似乎 OP 确实知道一些功能基础知识,而这个练习可能会成为过去的一个非常关键的障碍。通过nrkn遵循这一点,下面有一些很好的答案。这是 FP 更难的方面之一,但你会因此而变得更好。如果您真的想以认真的方式实现这一点,F# 可能不是最佳选择,但看起来您在这里设置了一个很好的练习。
  • 好的,谢谢大家,我会记住你的建议。根据你们所说的,我正在做一些进一步的阅读:matthewmanela.com/blog/functional-stateful-program-in-fstackoverflow.com/questions/3350644/… 如果没有你们有用的 cmets,我将无法找到这两个 :)

标签: f# functional-programming roguelike


【解决方案1】:

游戏编程通常会测试您管理复杂性的能力。我发现函数式编程鼓励您将解决的问题分解成更小的部分。

您要做的第一件事是通过分离所有不同的关注点,将您的脚本变成一堆函数。我知道这听起来很傻,但这样做的行为本身将使代码更具功能性(双关语)。您主要关心的是状态管理。我使用一条记录来管理位置状态和一个元组来管理运行状态。随着您的代码变得更高级,您将需要对象来干净地管理状态。

尝试在此游戏中添加更多内容,并随着功能的发展不断分解功能。最终,您将需要对象来管理所有功能。

在游戏编程笔记中,不要将状态更改为其他状态,如果测试失败则将其更改回来。你想要最小的状态变化。因此,例如下面我计算newPosition,然后仅在未来位置通过时更改playerPosition

open System

// use a third party vector class for 2D and 3D positions
// or write your own for pratice
type Pos = {x: int; y: int} 
    with
    static member (+) (a, b) =
        {x = a.x + b.x; y = a.y + b.y}

let drawBoard map =
    //clear the console
    Console.Clear()
    //send each row of the map to the Console
    map |> List.iter (printfn "%s")

let movePlayer (keyInfo : ConsoleKeyInfo) =
    match keyInfo.Key with
    | ConsoleKey.UpArrow -> {x = 0; y = -1}
    | ConsoleKey.DownArrow -> {x = 0; y = 1}
    | ConsoleKey.LeftArrow -> {x = -1; y = 0}
    | ConsoleKey.RightArrow  -> {x = 1; y = 0}
    | _ -> {x = 0; y = 0}

let validPosition (map:string list) position =
    map.Item(position.y).Chars(position.x) = ' '

//clear the square player was standing on
let clearPlayer position =
    Console.SetCursorPosition(position.x, position.y)
    Console.Write( ' ' )

//draw the square player is standing on
let drawPlayer position =
    Console.SetCursorPosition(position.x, position.y)
    Console.Write( '@' )

let takeTurn map playerPosition =
    let keyInfo = Console.ReadKey true
    // check to see if player wants to keep playing
    let keepPlaying = keyInfo.Key <> ConsoleKey.Q
    // get player movement from user input
    let movement = movePlayer keyInfo
    // calculate the players new position
    let newPosition = playerPosition + movement
    // check for valid move
    let validMove = newPosition |> validPosition map
    // update drawing if move was valid
    if validMove then
        clearPlayer playerPosition
        drawPlayer newPosition
    // return state
    if validMove then
        keepPlaying, newPosition
    else
        keepPlaying, playerPosition

// main game loop
let rec gameRun map playerPosition =
    let keepPlaying, newPosition = playerPosition |> takeTurn map 
    if keepPlaying then
        gameRun map newPosition

// setup game
let startGame map playerPosition =
    drawBoard map
    drawPlayer playerPosition
    gameRun map playerPosition


//define the map
let map = [ "                                        ";
            "                                        ";
            "                                        ";
            "                                        ";
            "    ###############################     ";
            "    #                             #     ";
            "    #         ######              #     ";
            "    #         #    #              #     ";
            "    #### #### #    #              #     ";
            "       # #  # #    #              #     ";
            "       # #  # #    #              #     ";
            "    #### #### ######              #     ";
            "    #              =              #     ";
            "    #              =              #     ";
            "    ###############################     ";
            "                                        ";
            "                                        ";
            "                                        ";
            "                                        ";
            "                                        " ]

//initial player position on the map
let playerPosition = {x = 8; y = 6}

startGame map playerPosition

【讨论】:

  • 这对我的函数式思维有很大帮助,一开始让我失望的是这一行:let playerPosition = {x = 8; y = 6} -- 如果我正确解释了代码,它并不是真正的持有者球员位置,这只是球员的起始位置。然后,在 takeTurn 中,您创建了一个循环,该循环使用玩家的新位置或每次迭代的最后位置。希望我是正确的,这将帮助其他人像我一样将他们的头脑围绕在函数式编程中。
【解决方案2】:

这是一个不错的小游戏 :-)。在函数式编程中,您希望避免使用可变状态(正如其他人指出的那样),并且您还希望将游戏的核心编写为没有任何副作用的函数(例如从控制台读取和写作)。

游戏的关键部分是控制位置的功能。您可以重构您的代码以具有具有类型签名的函数:

val getNextPosition : (int * int) -> ConsoleKey -> option<int * int>

如果游戏应该退出,该函数返回None。否则它返回Some(posX, posY),其中posXposY@ 符号的新位置。通过进行更改,您将获得一个不错的功能核心,并且函数 getNextPosition 也很容易测试(因为它总是为相同的输入返回相同的结果)。

要使用该函数,最好的选择是使用递归编写循环。 main 函数的结构如下所示:

let rec playing pos =
  match getNextPosition pos (Console.ReadKey()) with
  | None -> () // Quit the game
  | Some(newPos) ->
     // This function redraws the screen (this is a side-effect,
     // but it is localized to a single function)
     redrawScreen pos newPos
     playing newPos

【讨论】:

    【解决方案3】:

    作为一款游戏,并且使用控制台,这里存在固有的状态和副作用。但是你要做的关键是消除那些可变的。使用递归循环而不是 while 循环将帮助您做到这一点,因为您可以将您的状态作为参数传递给每个递归调用。除此之外,我可以看到在这里利用 F# 功能的主要内容是使用模式匹配而不是 if/then 语句和开关,尽管这主要是美学上的改进。

    【讨论】:

    • 构建整个地图,然后在单独的函数中打印也将更好地划分功能命令特征。
    【解决方案4】:

    我会尽量避免过于具体 - 如果我最终在另一个方向上走得太远并且这太模糊,请告诉我,我会尝试改进它。

    当制作一个有某种状态的函数式程序时,你想要实现的基本机制是这样的:

    (currentState, input) => newState
    

    然后您可以围绕它编写一个小包装器来处理获取输入和绘图输出。

    【讨论】:

      猜你喜欢
      • 2015-08-19
      • 2017-08-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-09-02
      • 2012-12-25
      • 2013-03-06
      • 1970-01-01
      相关资源
      最近更新 更多