简体   繁体   中英

Python - Printing an ASCII cartesian coordinate grid from a 2D array of position objects

Terribly sorry, all, but this is going to be a long one. So as an off-shoot of my previous inquiry regarding STDIN, unfortunately it doesn't appear there are many similar enough questions to my specific issue, at least not in Python (nor without using something like numpy which is a touch above my level). I do have plenty of code which I've already implemented, so hopefully that will help (although it's not the cleanest and as I mentioned above it's rather long).

My issue, as it stands, is with printing out ASCII-art representing a 2D array of class objects such that the ASCII accurately depicts them in the orientation of a simple x,y Cartesian coordinate grid.

I have several classes interacting with each other, the first of which constructs position objects containing two integers, representing the x and y of a Cartesian coordinate pair:

class Pos:
    def __init__(self, x, y):
        self.x = x
        self.y = y

The second class constructs a 'square' grid from some size parameter provided by the user in a different part of my program, implemented here as a 2D array filled with these position objects:

class Grid:
    def __init__(self, size):
        assert size > 0
        self.size = size
        self.grid = []
        #1  Perhaps there is something wrong with my 2D implementation...
        for x in range(size):
            sub_grid = []
            for y in range(size):
                sub_grid.append(Pos(x, y))
            self.grid.append(sub_grid)

Also within this class is a method which prints out ASCII-art of the grid to act as an external representation for the user (this is the messy bit):

def print(self):
    #  This is the main ASCII-art.
    size_check = self.size - 1
    if self.size > 10:
        print('   +' + ('-' * (((self.size * 2) + 1)) + '+'))
    else:
        print('  +' + ('-' * (((self.size * 2) + 1)) + '+'))
    counter = 0
    #2  ...or perhaps it's something wrong with this iterative condition.
    for x in self.grid[::-1]:
        if self.size > 10:
            if size_check > 9:
                print(self.size - 1 - counter, '|', end=' ')
            else:
                print('', self.size - 1 - counter, '|', end=' ')
        else:
            print(self.size - 1 - counter, '|', end=' ')
        counter += 1
        size_check -= 1
        for y in x:
            #  This is only here to check if the coords are in the correct position in the art.
            ^^if y.x == 5 and y.y == 8:
                print('O ', end='')^^
            else:
                print('. ', end='')
        print('|')
    if self.size > 10:
        print('   +' + ('-' * (((self.size * 2) + 1)) + '+'))
        print('     ', end='')
    else:
        print('  +' + ('-' * (((self.size * 2) + 1)) + '+'))
        print('    ', end='')

    #  This is simply support for grid sizes greater than 10.
    if self.size > 10:
        for x in self.grid:
            if x[0].x <= 9:
                print('', '', end=' ')
            elif x[0].x > 9 and x[0].x < (self.size - 1):
                strng = str(x[0].x)
                print(int(strng[0]), end=' ')
            elif x[0].x == (self.size - 1):
                strng = str(x[0].x)
                print(int(strng[0]))
        print('     ', end='')
        for x in self.grid:
            if x[0].x <= 9:
                print(x[0].x, '', end='')
            elif x[0].x > 9 and x[0].x < (self.size - 1):
                strng = str(x[0].x)
                print(int(strng[1]), end=' ')
            elif x[0].x == (self.size - 1):
                strng = str(x[0].x)
                print(int(strng[1]), end=' ')
    else:
        for x in self.grid:
            if x[0].x < (self.size - 1):
                print(x[0].x, '', end='')
            elif x[0].x == (self.size - 1):
                print(x[0].x, end='')
    print()

I know this is a lot, but what it successfully prints out looks like so (given size = 10):

  +---------------------+
9 | . . . . . . . . . . |
8 | . . . . . . . . . . |
7 | . . . . . . . . . . |
6 | . . . . . . . . . . |
5 | . . . . . . . . . . |
4 | . . . . . . . . . . |
3 | . . . . . . . . . . |
2 | . . . . . . . . . . |
1 | . . . . . . . . . . |
0 | . . . . . . . . . . |
  +---------------------+
    0 1 2 3 4 5 6 7 8 9

Now this looks just fine, until I try to show where the position objects are supposed to be located. In the above code I marked with ^^ a conditional statement which prints out an 'O' at the coordinates (5, 8). The result looks like so:

  +---------------------+
9 | . . . . . . . . . . |
8 | . . . . . . . . . . |
7 | . . . . . . . . . . |
6 | . . . . . . . . . . |
5 | . . . . . . . . O . |
4 | . . . . . . . . . . |
3 | . . . . . . . . . . |
2 | . . . . . . . . . . |
1 | . . . . . . . . . . |
0 | . . . . . . . . . . |
  +---------------------+
    0 1 2 3 4 5 6 7 8 9

The ideal output should actually look like this:

  +---------------------+
9 | . . . . . . . . . . |
8 | . . . . . O . . . . |
7 | . . . . . . . . . . |
6 | . . . . . . . . . . |
5 | . . . . . . . . . . |
4 | . . . . . . . . . . |
3 | . . . . . . . . . . |
2 | . . . . . . . . . . |
1 | . . . . . . . . . . |
0 | . . . . . . . . . . |
  +---------------------+
    0 1 2 3 4 5 6 7 8 9

As you can see, the ASCII-art is printing in such a way that externally it looks as if the x and y axis have swapped, even though internally the coords at any position object respond correctly. I wrote two comments in the above code where I speculate what the issue might be. My best guesses being either how I implemented the grid itself, or how I'm printing the art; but that's what I need help figuring out.

Any and all help is, as always, wholly appreciated; and I apologize once again!

I would suggest building your lines using string manipulation instead of printing directly. This will make the code simpler and allow for substitution of patterns within a template string for the typical line content.

For example:

# prepare the empty content
rows = 10
cols = 10
content = [["."]*cols for _ in range(rows)]

# assign values at coordinates as needed (based on your grid)
content[5][8]  = "O"
grid = [(4,1,"H"),(6,3,"L"),(5,2,"E"),(4,6,"R"),(7,4,"L"),(6,6,"W"),(3,6,"L"),(2,6,"D"),(5,6,"O")]
for (y,x,c) in grid: content[y][x] = c

# build frame
width       = len(str(max(rows,cols)-1))
contentLine = "# | values |"

dashes      = "-".join("-"*width for _ in range(cols))
frameLine   = contentLine.replace("values",dashes)
frameLine   = frameLine.replace("#"," "*width)
frameLine   = frameLine.replace("| ","+-").replace(" |","-+")

# print grid
print(frameLine)
for i,row in enumerate(reversed(content),1):
    values = " ".join(f"{v:{width}s}" for v in row)
    line   = contentLine.replace("values",values)
    line   = line.replace("#",f"{rows-i:{width}d}")
    print(line)
print(frameLine)

# x-axis numbers
numLine = contentLine.replace("|"," ")
numLine = numLine.replace("#"," "*width)
colNums = " ".join(f"{i:<{width}d}" for i in range(cols))
numLine = numLine.replace("values",colNums)
print(numLine)

output:

  +---------------------+
9 | . . . . . . . . . . |
8 | . . . . . O . . . . |
7 | . . . . L . . . . . |
6 | . . . L . . W . . . |
5 | . . E . . . O . . . |
4 | . H . . . . R . . . |
3 | . . . . . . L . . . |
2 | . . . . . . D . . . |
1 | . . . . . . . . . . |
0 | . . . . . . . . . . |
  +---------------------+
    0 1 2 3 4 5 6 7 8 9                  

output with rows=12 and cols=15:

   +----------------------------------------------+
11 | .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  |
10 | .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  |
 9 | .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  |
 8 | .  .  .  .  .  O  .  .  .  .  .  .  .  .  .  |
 7 | .  .  .  .  L  .  .  .  .  .  .  .  .  .  .  |
 6 | .  .  .  L  .  .  W  .  .  .  .  .  .  .  .  |
 5 | .  .  E  .  .  .  O  .  .  .  .  .  .  .  .  |
 4 | .  H  .  .  .  .  R  .  .  .  .  .  .  .  .  |
 3 | .  .  .  .  .  .  L  .  .  .  .  .  .  .  .  |
 2 | .  .  .  .  .  .  D  .  .  .  .  .  .  .  .  |
 1 | .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  |
 0 | .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  |
   +----------------------------------------------+
     0  1  2  3  4  5  6  7  8  9  10 11 12 13 14  

Here's an implementation using rust divided into few steps

1. The Grid struct

pub struct Grid {
    cells: Vec<Vec<char>>,
    num_rows: usize,
    num_cols: usize,
}

The Grid struct represents a grid of characters. It has three fields:

  • cells : a 2D vector of characters representing the cells in the grid.
  • num_rows : a usize representing the number of rows in the grid.
  • num_cols : a usize representing the number of columns in the grid.

2. The Alignment enum

#[derive(Debug, PartialEq)]
enum Alignment {
    Left,
    Center,
    Right,
}

The Alignment enum represents the possible alignments for the cell values when printing the grid. It has three variants:

  • Left : aligns the cell value to the left.
  • Center : aligns the cell value to the center.
  • Right : aligns the cell value to the right.

3. The new method

    fn new(num_rows: usize, num_cols: usize, empty_cell_char: char) -> Self {
        let cells = vec![vec![empty_cell_char; num_cols]; num_rows];
        Self {
            cells,
            num_rows,
            num_cols,
        }
    }
}

The new method is a constructor method for the Grid struct. It takes three parameters:

  • num_rows : a usize representing the number of rows in the grid.
  • num_cols : a usize representing the number of columns in the grid.
  • empty_cell_char : a char representing the character to fill empty cells.

It creates a new Grid object with the given number of rows and columns, and fills the cells with the given character.

4. The set_cell method

    fn set_cell(&mut self, row: usize, col: usize, character: char) {
        self.cells[row][col] = character;
    }
}

The set_cell method sets the character in a specific cell of the grid. It takes three parameters:

  • row : a usize representing the row number of the cell.
  • col : a usize representing the column number of the cell.
  • character : a char representing the character to set in the cell.

It sets the character in the cell at the given row and column.

5. The draw method

impl Grid {
    fn draw(
        &self,
        cell_width: usize,
        border_char: char,
        row_separator_char: char,
        col_separator_char: char,
        x_axis_separator_char: char,
        alignment: Alignment,
    ) {
        let frame_top_bottom = (0..self.num_cols)
            .map(|_| border_char.to_string().repeat(cell_width))
            .collect::<Vec<_>>()
            .join(&border_char.to_string());

        let row_template = format!(
            "{row_num}{row_separator}{row_values}{row_values_separator}",
            row_num = "{row_num}",
            row_separator = row_separator_char,
            row_values = "{row_values}",
            row_values_separator = row_separator_char
        );

        let row_num = row_template.replace(
            "{row_num}",
            format!("{:width$}", "", width = cell_width).as_str(),
        );

        let frame_top = row_num.replace("{row_values}", &frame_top_bottom);

        println!("{}", frame_top);

        for (row_num, row) in self.cells.iter().rev().enumerate() {
            let row_num_str = format!("{:width$}", self.num_rows - row_num - 1, width = cell_width);
            let row_values: Vec<String> = row
                .iter()
                .map(|cell| {
                    let aligned_string: String = match alignment {
                        Alignment::Left => format!("{:<width$}", cell, width = cell_width),
                        Alignment::Center => format!("{:^width$}", cell, width = cell_width),
                        Alignment::Right => format!("{:>width$}", cell, width = cell_width),
                    };

                    aligned_string
                })
                .collect();

            println!(
                "{}",
                row_template
                    .replace(
                        "{row_num}",
                        format!("{:<width$}", row_num_str, width = cell_width).as_str()
                    )
                    .replace(
                        "{row_values}",
                        &row_values.join(&col_separator_char.to_string())
                    )
            );
        }
        println!("{}", frame_top);

        let col_nums: Vec<String> = (0..self.num_cols)
            .map(|col| {
                let aligned_string: String = match alignment {
                    Alignment::Left => format!("{:<width$}", col, width = cell_width),
                    Alignment::Center => format!("{:^width$}", col, width = cell_width),
                    Alignment::Right => format!("{:>width$}", col, width = cell_width),
                };

                aligned_string
            })
            .collect();
        let x_axis = row_num.replace(
            "{row_values}",
            &col_nums.join(&x_axis_separator_char.to_string()),
        );
        println!("{}", x_axis);
    }
}

The draw method prints the grid to the console, with the given parameters. It takes six parameters:

  • cell_width : a usize representing the width of each cell when printing the grid.

  • border_char : a char representing the character to use for the border of the grid.

  • row_separator_char : a char representing the character to use as a separator between rows of the grid.

  • col_separator_char : a char representing the character to use as a separator between columns of the grid.

  • x_axis_separator_char : a char representing the character to use as a separator between the column numbers at the bottom of the grid.

  • alignment : an Alignment value representing the alignment of the cell values when printing the grid.

The draw method first generates the top and bottom border of the grid using the border_char and cell_width . It then creates a template for each row of the grid, which consists of the row number, a row separator, the cell values for that row, and a row values separator.

It then iterates over the rows of the grid, from bottom to top, and generates a string for each row using the template and the current row number and cell values. It then prints each row to the console.

Finally, it generates the column numbers at the bottom of the grid using the alignment and x_axis_separator_char , and prints them to the console.

6. The main function

Copy code

fn main() {
    let mut grid = Grid::new(3, 3, ' ');
    grid.set_cell(0, 0, 'X');
    grid.set_cell(1, 1, 'O');
    grid.set_cell(2, 2, 'X');
    grid.draw(
        4,
        '+',
        '|',
        ' ',
        ' ',
        Alignment::Right,
    );
}

The main function creates a new Grid object with 3 rows and 3 columns, and sets the characters in the cells using the set_cell method. It then calls the draw method on the Grid object to print it to the console, with a cell width of 4, a border character of '+' , a row separator character of '|' , a column separator character of ' ' , an x-axis separator character of ' ' , and an alignment of Alignment::Right .

Here is a summary of it in a table

Step Description
Define the Grid struct The Grid struct represents a grid of characters with three fields: cells , a 2D vector of characters representing the cells in the grid; num_rows , a usize representing the number of rows in the grid; and num_cols , a usize representing the number of columns in the grid.
Define the Alignment enum The Alignment enum represents the possible alignments for the cell values when printing the grid. It has three variants: Left , which aligns the cell value to the left; Center , which aligns the cell value to the center; and Right , which aligns the cell value to the right.
Define the new method The new method is a constructor for the Grid struct. It takes three parameters: num_rows , a usize representing the number of rows in the grid; num_cols , a usize representing the number of columns in the grid; and empty_cell_char , a char representing the character to fill empty cells. It creates a new Grid object with the given number of rows and columns, and fills the cells with the given character.
Define the set_cell method The set_cell method sets the character in a specific cell of the grid. It takes three parameters: row , a usize representing the row number of the cell; col , a usize representing the column number of the cell; and character , a char representing the character to set in the cell. It sets the character in the cell at the given row and column.
Define the draw method The draw method prints the grid to the console, with the given parameters. It takes six parameters: cell_width , a usize representing the width of each cell when printing the grid; border_char , a char representing the character to use for the border of the grid; row_separator_char , a char representing the character to use as a separator between rows of the grid; col_separator_char , a char representing the character to use as a separator between columns of the grid; x_axis_separator_char , a char representing the character to use as a separator between the column numbers at the bottom of the grid; and alignment , an Alignment value representing the alignment of the cell values when printing the grid. It first generates the top and bottom border of the grid using the border_char and cell_width . It then creates a template for each row of the grid, which consists of the row number, a row separator, the cell values for that row, and a row values separator. It then iterates over the rows of the grid, from bottom to top, and generates a string for each row using the template and the current row number and cell values. It then prints each row to the console. Finally, it generates the column numbers at the bottom of the grid using the alignment and x_axis_separator_char , and prints them to the console.
Define the main function The main function creates a new Grid object with 3 rows and 3 columns, and sets the characters in the cells using the set_cell method. It then calls the draw method on the Grid object to print it to the console, with a cell width of 4, a border character of '+' , a row separator character of `'

Here is ultra summary of the table

Step Description
Define the Grid struct The Grid struct represents a grid of characters with three fields: cells , a 2D vector of characters representing the cells in the grid; num_rows , a usize representing the number of rows in the grid; and num_cols , a usize representing the number of columns in the grid.
Define the Alignment enum The Alignment enum represents the alignment of the cell values when printing the grid. It has three variants: Left , Center , and Right .
Define the new method The new method creates a new Grid object with the given number of rows and columns, and fills the cells with a given character.
Define the set_cell method The set_cell method sets the character in a specific cell of the grid.
Define the draw method The draw method prints the grid to the console with the given parameters.
Define the main function The main function creates a Grid object and sets the characters in its cells. It then prints the grid to the console with the draw method.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM