Remarks
The code is readable, and appears to be correct. That's a pretty good starting point. It's shorter than what's presented here.
I'm always skeptical of the word "Pythonic". The original code may be more Pythonic than that presented here because it is more explicitly procedural. Then again, someone could come along and claim that because Python has objects, true Pythonic code is object oriented. Down at the level of a single statement or expression, there are some dominant idioms. But up at the level of programs "Pythonic" is mostly part of a pejorative "Not Pythonic".
The problem is such that there's no good way of avoiding the fact that there are four cases. Where the code in the question is weak is the level of abstraction at which the four cases are articulated. There's lots of fiddly-bits around columns and rows and limits up near the top-level of the function. Even though our top level abstraction is a diagonal. The mathematics works, but it's not obvious exactly why.
One way of thinking about Remark 2 is that the code 'one level down' is reductionist rather than compositional. It expresses itself in terms a bit more like 'machine code' than 'business logic'...or perhaps the mathematical logic suggested when linear algebra was mentioned.
A More Mathematical Approach
Because, limitations of computer hardware aside, the chessboard can be arbitrarily large, looking at the solution for a bishop on an infinitely large chessboard might be a place to begin looking for a general solution.
The first thing that popped out is that the choice of coordinate system origin point is important because an infinitely sized chessboard does not have any corners. Since the choice of origin point is arbitrary, maybe placing the Bishop at (0,0) might things easier to understand. It gives us a Cartesian grid:
|
IV | I
|
|
---------+----------
|
|
III | II
|
|
Cartesian Space
The bishop's diagonals are defined by simple functions.
\ | /
\ | /
\ | / y = x
\ | /
---------+----------
/|\
/ | \
/ | \ y = -x
/ | \
/ | \
Bishop Diagonals as the Union of Two Functions
And a board of finite size can be visualized as:
|
+----+----+
| | |
| | |
----+----+----+-----
| | |
+----+----+
|
|
|
Clipped Cartesian Space
A bit of Linear Algebra
Since it was mentioned in the question, perhaps there are some useful abstractions there. It is possible to think of y = x
and y = -x
as two matrices. Each has two columns and an infinite number of rows. Going further y = x
can be transformed into the other matrix by the 2x1 matrix [1,-1] where the values are arranged in a column.
Going back to the original Cartesian grid, we translate the solution for the portion of the diagonal lying in one quadrant, into the relevant diagonal for the other column:
def diagonals(coord, size):
"""generate the worst case solution
for quadrant I (northeast) where
each value is (+,+)"""
base = zip(range(1, size),range(1, size))
max_index = size - 1
The other diagonals in the other quadrants can be generated from the worst case diagonal for Quadrant I (northeast). Technically, the transformation can be seen as either a four-fold rotation or two mirror planes. I thought that rotation seemed easier.
"""rotation matrices"""
northeast = (1,1)
southeast = (-1,1)
southwest = (-1,-1)
northwest = (1,-1)
def rotate_diagonal(diagonal,direction):
def rotate(coord_i):
return (coord_i[0] * direction[0], coord_i[1] * direction[1])
return map(rotate, diagonal)
Each diagonal gets clipped at the board boundaries based on the distance of the bishop from the edges. The distance of the bishop from the edges is defined by it's coordinate value.
def clip_diagonal(direction):
a = coord[0] if direction[0] < 0 else max_index - coord[0]
b = coord[1] if direction[1] < 0 else max_index - coord[1]
return base[:min(a,b)]
The last piece is to undo my arbitrary choice of origin. It boils down to a simple 2d translation. If the bishop is on square (i,j)
then the equations become y + j = x + i
and y + j = -x + i
.
def translate_diagonal(diagonal):
def translate(coord_i):
return (coord[0] + coord_i[0], coord[1] + coord_i[1])
return map(translate, diagonal)
Then all that's left to do is to make the diagonals for each quadrant. Clipping is applied first to reduce the number of calculations where possible. Translation is applied last because rotation produces negative values.
def make_diagonal(direction):
clipped = clip_diagonal(direction)
rotated = rotate_diagonal(clipped, direction)
return translate_diagonal(rotated)
return [coord]+make_diagonal(northeast)+make_diagonal(southeast)+make_diagonal(southwest)+make_diagonal(northwest)
The last line is dreadful, I think, but clear enough I suppose. That's the price of staying onside.