Building a command line tool to design a farm layout in Stardew Valley

栏目: IT技术 · 发布时间: 4年前

内容简介:By John Lekberg on February 26, 2020.This week's post will cover a command line tool that helps you play the video gameStardew Valley is a farming game (like

By John Lekberg on February 26, 2020.

This week's post will cover a command line tool that helps you play the video game Stardew Valley .

Stardew Valley is a farming game (like Harvest Moon ). You can manually water your crops, or you can use sprinklers to automate the process.

I wrote a Python script, sprinkler-layout , that designs a layout of sprinklers for me, for a given number of sprinklers (e.g. 10 sprinklers). The goals of the layout are:

  • water as much land as possible.
  • have a reasonably small perimeter.

Script source code

sprinkler-layout

#!/usr/bin/env python3

import itertools


class Layout:
    """A layout of sprinklers on a grid."""

    def __init__(self):
        self._sprinklers = set()
        self._watered_squares = set()

    @classmethod
    def generate(cls, *, num_sprinklers, coordinates):
        """Generate a layout, given:

        - how many sprinklers to place.
        - which positions to attempt to place them at.

        """
        layout = cls()

        while layout.count_sprinklers() < num_sprinklers:
            position = next(coordinates)
            if layout.is_open(position):
                layout.add_sprinkler(position)

        return layout

    def count_sprinklers(self):
        """The current number of placed sprinklers."""
        return len(self._sprinklers)

    def _watering_positions(self, sprinkler_position):
        """Generate positions watered by a sprinkler at a
        given position."""
        x, y = sprinkler_position
        yield x - 1, y
        yield x + 1, y
        yield x, y - 1
        yield x, y + 1

    def is_open(self, position):
        """Check if a position is open for placing a
        sprinkler.

        A position is open if

        - the set of the position and its watered squares

        does not intersect with

        - the set of already placed sprinklers and their
          watered squares.

        """
        new_positions = {position, *self._watering_positions(position)}
        return not (
            new_positions & (self._sprinklers | self._watered_squares)
        )

    def add_sprinkler(self, position):
        """Add a sprinkler at a position."""
        self._sprinklers.add(position)
        self._watered_squares.update(
            self._watering_positions(position)
        )

    def print_report(self):
        """Print out a report of the current layout.

        The report includes:

        - The dimensions of the layout.
        - The materials cost of the layout.
        - A visualization of the layout.

        """
        squares = self._sprinklers | self._watered_squares
        X = [x for x, _ in squares]
        Y = [y for _, y in squares]
        span = lambda Z: range(min(Z), max(Z) + 1)
        grid = [
            [
                "#" if (x, y) in self._sprinklers else "."
                for x in span(X)
            ]
            for y in span(Y)
        ]
        width = len(span(X)) + 2
        height = len(span(Y)) + 2

        print(len(self._sprinklers), "sprinklers")
        print(len(self._watered_squares), "watered squares")
        print(width, "x", height, "squares, including perimeter wall")
        print(2 * (width + height), "square perimeter")

        block = 3
        print(f"map of sprinklers ({block} by {block} blocks)")
        for i, row in enumerate(grid):
            if i % block == 0:
                print()
            for j, square in enumerate(row):
                if j % block == 0:
                    print(end=" ")
                print(square, end="")
            print()
        print()


def spiral_coordinates():
    """Generate positions along a spiral.
    
    The first nine steps of the spiral look like this

        7 6 5  |  v < <
        8 1 4  |  v v ^
        9 2 3  |  v > ^

    """
    yield 0, 0
    for radius in itertools.count(start=1):
        x, y = 1 - radius, radius
        while x < radius:
            yield x, y
            x += 1
        while y > -radius:
            yield x, y
            y -= 1
        while x > -radius:
            yield x, y
            x -= 1
        while y < radius:
            yield x, y
            y += 1

        yield x, y


if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(
        description=
            "generate a layout of sprinklers for Stardew Valley."
    )
    parser.add_argument(
        "--sprinklers",
        type=int,
        required=True,
        metavar="N",
        help="the number of sprinklers to place",
    )
    args = parser.parse_args()

    layout = Layout.generate(
        num_sprinklers=args.sprinklers,
        coordinates=spiral_coordinates()
    )
    layout.print_report()
$ sprinkler-layout --help
usage: sprinkler-layout [-h] --sprinklers N

generate a layout of sprinklers for Stardew Valley.

optional arguments:
  -h, --help      show this help message and exit
  --sprinklers N  the number of sprinklers to place

Using the script to design a layout

I'm starting a new farming season in Stardew Valley and I have 25 sprinklers available. I use sprinkler-layout to design a layout:

$ sprinkler-layout --sprinklers 25
25 sprinklers
100 watered squares
16 x 17 squares, including perimeter wall
66 square perimeter
map of sprinklers (3 by 3 blocks)

 ... ... ... ... ..
 ... ... ... ... #.
 ..# ..# ... ... ..

 ... ... .#. ..# ..
 ... #.. ... #.. ..
 .#. ..# ... ... ..

 ... ... ..# ..# ..
 ... #.. #.. ... ..
 .#. ... ... .#. ..

 ... ..# ..# ... ..
 ... #.. ... ... #.
 ... ... #.. #.. ..

 ..# ... ... ..# ..
 ... .#. .#. ... ..
 ... ... ... ... ..

Then:

#

How the script works

I use a custom class, Layout , to represent a sprinkler layout. Layout manages the internal state of:

  • Where sprinklers have been placed.
  • Which positions are watered by the placed sprinklers.

Layout has a class method , generate , that attempts to position sprinklers by choosing from given positions. generate uses a greedy strategy to place the sprinklers:

  • Loop until I have placed enough sprinklers:
    • Get the next position to try.
    • If I can place a sprinkler at this position, do it.

I check if I can place a sprinkler by using sets of coordinates and checking that these sets are disjoint :

  • the set of the new sprinkler and its watered squares.
  • the set of already placed sprinklers and their watered squares.

I have a generator function, spiral_coordinates , the generates positions in a spiral that looks like this: (starting from the center)

v < < < < < <
v v < < < < ^
v v v < < ^ ^
v v v v ^ ^ ^
v v v > ^ ^ ^
v v > > > ^ ^
v > > > > > ^
> ...

I use this technique because it designs good enough layouts for me. spiral_coordinates is simple to implement and keeps the overall perimeter of the layout small.

The report function, print_report , computes a bounding box that encloses:

  • the sprinklers that have been placed.
  • the squares that are watered by the placed sprinklers.

Then, I take into account a 1 square thick perimeter wall and report:

  • The dimensions of the bounding box.
  • The perimeter of the bounding box.

The report generates a map of the placed sprinklers and partitions it into chunks:

... ... ... ... ..
 ... ... ... ... #.
 ..# ..# ... ... ..

 ... ... .#. ..# ..
 ... #.. ... #.. ..
 .#. ..# ... ... ..

 ... ... ..# ..# ..
 ... #.. #.. ... ..
 .#. ... ... .#. ..

 ... ..# ..# ... ..
 ... #.. ... ... #.
 ... ... #.. #.. ..

 ..# ... ... ..# ..
 ... .#. .#. ... ..
 ... ... ... ... ..

I find the map harder to read without the partitioning:

..............
............#.
..#..#........
.......#...#..
...#.....#....
.#...#........
........#..#..
...#..#.......
.#........#...
.....#..#.....
...#........#.
......#..#....
..#........#..
....#..#......
..............

In conclusion...

This week's post covered a Python script that assists people playing Stardew Valley by designing a layout of sprinklers. You learned about:

  • Using Python classes to manage internal state.
  • Using Python sets to check if two sets of positions are disjoint.
  • Using a simple greedy strategy to make decisions (placing the sprinklers).

My challenge to you:

Create a different way to generate coordinates than spiral_coordinates .

For example, here's what a placement of 8 sprinklers looks like with spiral_coordinates :

Layout.generate(
    num_sprinklers = 8,
    coordinates = spiral_coordinates()
).print_report()
8 sprinklers
32 watered squares
11 x 10 squares, including perimeter wall
42 square perimeter
map of sprinklers (3 by 3 blocks)

 ... ... ...
 .#. ... .#.
 ... #.. ...

 ... ... #..
 .#. .#. ...
 ... ... ...

 ... #.. #..
 ... ... ...

And here's a placement of 8 sprinklers that tries positions only in a horizontal line:

from itertools import count

Layout.generate(
    num_sprinklers = 8,
    coordinates = ((i, 0) for i in count())
).print_report()
8 sprinklers
32 watered squares
26 x 5 squares, including perimeter wall
62 square perimeter
map of sprinklers (3 by 3 blocks)

 ... ... ... ... ... ... ... ...
 .#. .#. .#. .#. .#. .#. .#. .#.
 ... ... ... ... ... ... ... ...

If you enjoyed this week's post, share it with your friends and stay tuned for next week's post. See you then!


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Starfish and the Spider

The Starfish and the Spider

Ori Brafman、Rod A. Beckstrom / Portfolio Hardcover / 2006-10-05 / USD 24.95

Understanding the amazing force that links some of today's most successful companies If you cut off a spider's leg, it's crippled; if you cut off its head, it dies. But if you cut off a st......一起来看看 《The Starfish and the Spider》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具