Chess game challenge OOP from Exercism dot org

Hello Friends!

I’m wrestling with a chess game challenge involving one queen facing off another, with no other pieces involved. The exercise provides unit tests to work with (included at the very bottom of this post). It’s not a formal homework assignment for a classroom setting, but I am trying to pretend that it is, meaning that I don’t want you people to complete the assignment for me. Instead, please just share hints, tips, pointers, and general guidance. Thanks.

Here are the instructions for the exercise:

That’s the challenge I am working on. I understand the basic concept where the white queen is positioned at c5 (zero-indexed) at column 2, row 3 and how that corresponds to the text based chess board grid. What I need clarification on is the meaning/use of ‘zero-indexed’. Could someone elaborate on zero-indexed in this context? I understand the principle of indexing of strings and lists (arrays) but in the context would the index be pointing to the queen’s location at the coordinartes?

For the dunder __init__ method, I’ve started with this in my script:

class Queen:
   def __init__(self, row, column):
       self.row = row
       self.column = column

I get the sense that the above class attribute declaration isn’t enough. Instead, should I be creating and initializing a 1-8 and a-h matrix or grid board like something more sophisticated than the basic 10x10 checkerboard grid below? The checkers board below is just alternating on/off, zeroes and ones that can be built inside a REPL that I found on Stack Overflow. For the original chess challenge described above, do I need to build a similar grid using hashtables but with 64 unique address locations 1-8 (verticle axis) and a-h (horizontal axis)?

>>> import numpy as NP
>>> def build_checkerboard(w, h) :
     re = NP.r_[ w*[0,1] ]              # even-numbered rows
     ro = NP.r_[ w*[1,0] ]              # odd-numbered rows
     return NP.row_stack(h*(re, ro))
>>> checkerboard = build_checkerboard(5, 5)
>>> checkerboard
Out[3]: array([[0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
              [1, 0, 1, 0, 1, 0, 1, 0, 1, 0],
              [0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
              [1, 0, 1, 0, 1, 0, 1, 0, 1, 0],
              [0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
              [1, 0, 1, 0, 1, 0, 1, 0, 1, 0],
              [0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
              [1, 0, 1, 0, 1, 0, 1, 0, 1, 0],
              [0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
              [1, 0, 1, 0, 1, 0, 1, 0, 1, 0]])

My (horribly incomplete) script looks like this currently:

class Queen:
   def __init__(self, row, column):
       self.row = row
       self.column = column

   def can_attack(self, another_queen):
       if self.row < 0: # if the row parameter is negative
           raise ValueError("row not positive")

       if self.row > 8 or self.row < 0: # if the row parameter is not on the defined board
           raise ValueError("row not on board")

       if self.column < 0: # if the column parameter is negative
           raise ValueError("column not positive")

       if self.column < 0 or self.column > 8: # if the column parameter is not on the defined board
           raise ValueError("column not on board")

       # if both the queens are on the same location
       raise ValueError("Invalid queen position: both queens in the same square")

Naturally, it fails 13 of 14 unit tests. I clearly have a lot of work cut out for me.

Let’s just take the second Assertion to see if I understand what it is trying to test:

   def test_queen_must_have_positive_row(self):
       with self.assertRaises(ValueError) as err:
           Queen(-2, 2)
       self.assertEqual(type(err.exception), ValueError)
       self.assertEqual(err.exception.args[0], "row not positive")

Two arguments are being passed into the Queen class: -2 and 2. Since 2 is the self.row attribute and it is less than 0, it is out of parameters of the chess board. But as far as I can tell, the first line beneath my can_attack() class method accounts for the scenario. When the row is negative, I’ve directed my script to throw the “row not positive” ValueError. Or at least that’s my intention anyways. The gap in my current script is how to handle the another_queen variable and integrate that into the conditional logic for ValueErrors. Any hints or suggestions in this regard are welcome. That is my ask for all of you reading this.

Here is the full unit test that comes with the challenge:

import unittest

from queen_attack import (
    Queen,
)

# Tests adapted from `problem-specifications//canonical-data.json`


class QueenAttackTest(unittest.TestCase):
    # Test creation of Queens with valid and invalid positions
    def test_queen_with_a_valid_position(self):
        Queen(2, 2)

    def test_queen_must_have_positive_row(self):
        with self.assertRaises(ValueError) as err:
            Queen(-2, 2)
        self.assertEqual(type(err.exception), ValueError)
        self.assertEqual(err.exception.args[0], "row not positive")

    def test_queen_must_have_row_on_board(self):
        with self.assertRaises(ValueError) as err:
            Queen(8, 4)
        self.assertEqual(type(err.exception), ValueError)
        self.assertEqual(err.exception.args[0], "row not on board")

    def test_queen_must_have_positive_column(self):
        with self.assertRaises(ValueError) as err:
            Queen(2, -2)
        self.assertEqual(type(err.exception), ValueError)
        self.assertEqual(err.exception.args[0], "column not positive")

    def test_queen_must_have_column_on_board(self):
        with self.assertRaises(ValueError) as err:
            Queen(4, 8)
        self.assertEqual(type(err.exception), ValueError)
        self.assertEqual(err.exception.args[0], "column not on board")

    # Test the ability of one queen to attack another
    def test_cannot_attack(self):
        self.assertIs(Queen(2, 4).can_attack(Queen(6, 6)), False)

    def test_can_attack_on_same_row(self):
        self.assertIs(Queen(2, 4).can_attack(Queen(2, 6)), True)

    def test_can_attack_on_same_column(self):
        self.assertIs(Queen(4, 5).can_attack(Queen(2, 5)), True)

    def test_can_attack_on_first_diagonal(self):
        self.assertIs(Queen(2, 2).can_attack(Queen(0, 4)), True)

    def test_can_attack_on_second_diagonal(self):
        self.assertIs(Queen(2, 2).can_attack(Queen(3, 1)), True)

    def test_can_attack_on_third_diagonal(self):
        self.assertIs(Queen(2, 2).can_attack(Queen(1, 1)), True)

    def test_can_attack_on_fourth_diagonal(self):
        self.assertIs(Queen(1, 7).can_attack(Queen(0, 6)), True)

    def test_cannot_attack_if_falling_diagonals_are_only_the_same_when_reflected_across_the_longest_falling_diagonal(
        self,
    ):
        self.assertIs(Queen(4, 1).can_attack(Queen(2, 5)), False)

    # Track-specific tests
    def test_queens_same_position_can_attack(self):
        with self.assertRaises(ValueError) as err:
            Queen(2, 2).can_attack(Queen(2, 2))
        self.assertEqual(type(err.exception), ValueError)
        self.assertEqual(
            err.exception.args[0],
            "Invalid queen position: both queens in the same square",
        )

Hello, @enoren5. I hope, this will help you:

#If their column OR row numbers are same, we can say 
#that they can attack each other.(but their coordinates 
#mustn't be completely same)

#Or, if they are both on a diagonal line, they can also attack each other.
# I think, you can apply a basic test to see if they provide the condition:
# Think that your first coordinates are (3,5) and second ones are (6,2).
# If you find and highlight these coordinates on a board, you will see that that they're 
# in a diagonal position. Because (3-6) = -(5-2).
#  FORMULA: 
#row - another row = - (column- another column)
# NOTE: Because you are working with list indexes, that would be better to add 1 to your indexes while you are testing them.

# A little reminder:
# If you have an object method, which should use another object's attributes, you can simply get them with this classic way:
# other.attr_name
#Example:
class Person:
	def __init__(self,name,age):
		self.name = name
		self.age = age
	def bring_together(self,other):
		#if "other" is also a person object, 
		#it also has the same attributes.(name and age)
		new_name = self.name + " and " + other.name 
		new_age = str(self.age) + " and " + str(other.age)
		return (new_name,new_age)

person1 = Person("John Doe",30)
person2 = Person("Mary Doe",26)
double_person = person1.bring_together(person2)
print(double_person)

#In your case, we are sure that the method will only take one type of object: queen. So, you don't have to worry about other object types.

One important point:
You should put “validity error statements” into your init function. Because tests want you to directly error out the invalid arguments:

def test_queen_must_have_positive_row(self):
    with self.assertRaises(ValueError) as err:
        Queen(-2, 2)
    self.assertEqual(type(err.exception), ValueError)
    self.assertEqual(err.exception.args[0], "row not positive")

One more point:
I really don’t think that you should work with lists-I think ways above are enough-.But, to pass tests, your code should behave like it works with lists. -I say this because tests want you to raise an error if one of your arguments is 8.-
So, your invalid arguments are the ones,

  • bigger than 7(an 8-membered list’s last index is 7)
  • and less than 0

But your code says, 8 is a valid argument:

if self.row > 8 or self.row < 0: # if the row parameter is not on the defined board
raise ValueError(“row not on board”)