Antwoord op: Kat & Muis
Wit kan altijd winnen als het een goede strategie hanteert (wat niet het geval was bij het spel dat je zojuist bij de puzzel hebt gespeeld). Je kunt dit zelf ervaren door hier het spel tegen de computer te spelen (klik op het veld waar je met het zwarte speelstuk naartoe wilt gaan).
Hieronder volgt een toelichting. Er zijn drie mogelijke uitkomsten:
- Ja, het spel is berekenbaar en wit kan altijd winnen.
- Ja, het spel is berekenbaar en zwart kan altijd winnen.
- Nee, het spel is niet berekenbaar.
We definiëren eerst Wwit(spelsituatie) en Wzwart(spelsituatie), die aangeven of een speler gewonnen heeft in een bepaalde spelsituatie:
- Wwit(spelsituatie) geldt dan en slechts dan als wit in de spelsituatie gewonnen heeft (dat wil zeggen dat zwart volledig klemgezet is).
- Wzwart(spelsituatie) geldt dan en slechts dan als zwart in de spelsituatie gewonnen heeft (dat wil zeggen dat zwart de overkant bereikt heeft).
We definiëren verder Cwit(speler aan zet, spelsituatie) en Czwart(speler aan zet, spelsituatie).
Cwit(speler aan zet, spelsituatie) geeft aan of wit altijd kan winnen in een spelsituatie waarin een bepaalde speler aan zet is:
- Cwit(wit, spelsituatie) geldt dan en slechts dan als Wzwart(spelsituatie) niet geldt en er een witte zet m in de spelsituatie bestaat waarvoor Cwit(zwart, "spelsituatie na zet m") geldt.
- Cwit(zwart, spelsituatie) geldt dan en slechts dan als Wwit(spelsituatie) geldt of voor alle mogelijke zwarte zetten m in de spelsituatie, Cwit(wit, "spelsituatie na zet m") geldt.
Czwart(speler aan zet, spelsituatie) geeft aan of zwart altijd kan winnen in een spelsituatie waarin een bepaalde speler aan zet is:
- Czwart(wit, spelsituatie) geldt dan en slechts dan als Wzwart(spelsituatie) geldt of voor alle mogelijke witte zetten m in de spelsituatie, Czwart(zwart, "spelsituatie na zet m") geldt.
- Czwart(zwart, spelsituatie) geldt dan en slechts dan als Wwit(spelsituatie) niet geldt en er een zwarte zet m in de spelsituatie bestaat waarvoor Czwart(wit, "spelsituatie na zet m") geldt.
Nu geldt:
- Het spel is berekenbaar en wit kan altijd winnen als Cwit(zwart, beginsituatie).
- Het spel is berekenbaar en zwart kan altijd winnen als Czwart(zwart, beginsituatie).
- Het spel is niet berekenbaar als Cwit(zwart, beginsituatie) en Czwart(zwart, beginsituatie) beide niet gelden.
Met vijf speelstukken en 32 velden zijn er maximaal 32 × 31 × 30 × 29 × 28 = 24.165.120 mogelijke spelsituaties. Een computerprogramma kan alle mogelijke spelverlopen dus in korte tijd doorrekenen en voor elke spelsituatie Cwit(speler aan zet, spelsituatie) en Czwart(speler aan zet, spelsituatie) bepalen. Met behulp van een dergelijk programma kan worden vastgesteld dat Cwit(zwart, beginsituatie) geldt, en dat dus wit altijd kan winnen. Hieronder staat een voorbeeld van een Python-programma dat hiervoor gebruikt kan worden.
Het is zelfs mogelijk om te berekenen hoe lang zwart het kan uithouden: als wit slim speelt, is het spel na maximaal 44 zetten voobij.
# BSD 3-Clause License
#
# Copyright 2025 RJE-Productions and E.R. van Veldhoven
#
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
# following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
# following disclaimer in the documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
# products derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
from enum import Enum
# We define a board of 10 by 10 fields as shown in the figure below. This board consists of an actual 8 by 8
# chessboard, surrounded by an outer ring of 'invalid' fields. The black fields of the chessboard are not used.
# In the figure, both the 'invalid' and the 'unused' fields have their numbers surrounded by brackets.
#
# +----+----+----+----+----+----+----+----+----+----+
# | (0)| (1)| (2)| (3)| (4)| (5)| (6)| (7)| (8)| (9)|
# +----+----+----+----+----+----+----+----+----+----+
# |(10)| 11 |(12)| 13 |(14)| 15 |(16)| 17 |(18)|(19)|
# +----+----+----+----+----+----+----+----+----+----+
# |(20)|(21)| 22 |(23)| 24 |(25)| 26 |(27)| 28 |(29)|
# +----+----+----+----+----+----+----+----+----+----+
# |(30)| 31 |(32)| 33 |(34)| 35 |(36)| 37 |(38)|(39)|
# +----+----+----+----+----+----+----+----+----+----+
# |(40)|(41)| 42 |(43)| 44 |(45)| 46 |(47)| 48 |(49)|
# +----+----+----+----+----+----+----+----+----+----+
# |(50)| 51 |(52)| 53 |(54)| 55 |(56)| 57 |(58)|(59)|
# +----+----+----+----+----+----+----+----+----+----+
# |(60)|(61)| 62 |(63)| 64 |(65)| 66 |(67)| 68 |(69)|
# +----+----+----+----+----+----+----+----+----+----+
# |(70)| 71 |(72)| 73 |(74)| 75 |(76)| 77 |(78)|(79)|
# +----+----+----+----+----+----+----+----+----+----+
# |(80)|(81)| 82 |(83)| 84 |(85)| 86 |(87)| 88 |(89)|
# +----+----+----+----+----+----+----+----+----+----+
# |(90)|(91)|(92)|(93)|(94)|(95)|(96)|(97)|(98)|(99)|
# +----+----+----+----+----+----+----+----+----+----+
#
# The figure below only shows the numbers of the 32 used fields:
#
# +----+----+----+----+----+----+----+----+----+----+
# | | | | | | | | | | |
# +----+----+----+----+----+----+----+----+----+----+
# | | 11 | | 13 | | 15 | | 17 | | |
# +----+----+----+----+----+----+----+----+----+----+
# | | | 22 | | 24 | | 26 | | 28 | |
# +----+----+----+----+----+----+----+----+----+----+
# | | 31 | | 33 | | 35 | | 37 | | |
# +----+----+----+----+----+----+----+----+----+----+
# | | | 42 | | 44 | | 46 | | 48 | |
# +----+----+----+----+----+----+----+----+----+----+
# | | 51 | | 53 | | 55 | | 57 | | |
# +----+----+----+----+----+----+----+----+----+----+
# | | | 62 | | 64 | | 66 | | 68 | |
# +----+----+----+----+----+----+----+----+----+----+
# | | 71 | | 73 | | 75 | | 77 | | |
# +----+----+----+----+----+----+----+----+----+----+
# | | | 82 | | 84 | | 86 | | 88 | |
# +----+----+----+----+----+----+----+----+----+----+
# | | | | | | | | | | |
# +----+----+----+----+----+----+----+----+----+----+
#
# White starts on the fields 11, 13, 15, and 17. We store the positions of the white pieces in the 'whitePositions'
# array. Black starts on field 84, and we store its position in the 'blackPosition' variable.
#
# White can make the following two moves:
# 1. one field diagonally down to the left: this corresponds to an increase of the field number by 9.
# 2. one field diagonally down to the right: this corresponds to an increase of the field number by 11.
#
# Black can make the following four moves:
# 1. one field diagonally down to the left: this corresponds to an increase of the field number by 9.
# 2. one field diagonally down to the right: this corresponds to an increase of the field number by 11.
# 3. one field diagonally up to the left: this corresponds to a decrease of the field number by 11.
# 4. one field diagonally up to the right: this corresponds to a decrease of the field number by 9.
#
# A move is only valid if there is not already a piece on the destination field and the destination field is not on
# the outer ring. A field lies on the outer ring if any of the following conditions hold:
# - the field number is less than 10;
# - the field number is at least 90;
# - the field number modulo 10 equals 0;
# - the field number modulo 10 equals 9.
#
# We store the result for each game situation in the variable "outcomes". This variable contains the outcome for
# each combination (player to move, position of black, positions of white pieces 1 to 4). We map the 32 possible
# field numbers to the range [0..31] as follows:
#
# +----+----+----+----+----+----+----+----+----+----+
# | | | | | | | | | | |
# +----+----+----+----+----+----+----+----+----+----+
# | | 0 | | 1 | | 2 | | 3 | | |
# +----+----+----+----+----+----+----+----+----+----+
# | | | 4 | | 5 | | 6 | | 7 | |
# +----+----+----+----+----+----+----+----+----+----+
# | | 8 | | 9 | | 10 | | 11 | | |
# +----+----+----+----+----+----+----+----+----+----+
# | | | 12 | | 13 | | 14 | | 15 | |
# +----+----+----+----+----+----+----+----+----+----+
# | | 16 | | 17 | | 18 | | 19 | | |
# +----+----+----+----+----+----+----+----+----+----+
# | | | 20 | | 21 | | 22 | | 23 | |
# +----+----+----+----+----+----+----+----+----+----+
# | | 24 | | 25 | | 26 | | 27 | | |
# +----+----+----+----+----+----+----+----+----+----+
# | | | 28 | | 29 | | 30 | | 31 | |
# +----+----+----+----+----+----+----+----+----+----+
# | | | | | | | | | | |
# +----+----+----+----+----+----+----+----+----+----+
NR_OF_POSITIONS = 32 # There are 32 fields on which the pieces can be positioned.
POSSIBLE_MOVES = [9, 11, -11, -9]
# Define a mapping of the 32 usable field numbers in the range [0..99] to the range [0..31].
MAPPING = [
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1, 1, -1, 2, -1,
3, -1, -1, -1, -1, 4, -1, 5, -1, 6, -1, 7, -1, -1, 8, -1, 9, -1,
10, -1, 11, -1, -1, -1, -1, 12, -1, 13, -1, 14, -1, 15, -1, -1,
16, -1, 17, -1, 18, -1, 19, -1, -1, -1, -1, 20, -1, 21, -1, 22, -1,
23, -1, -1, 24, -1, 25, -1, 26, -1, 27, -1, -1, -1, -1, 28, -1,
29, -1, 30, -1, 31, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1
]
class Player(Enum):
BLACK = 0
WHITE = 1
class Outcome(Enum):
NOT_CALCULATED = 0
BLACK_CAN_ALWAYS_WIN = 1
WHITE_CAN_ALWAYS_WIN = 2
BEING_CALCULATED = 3
class CatAndMouseGame:
def __init__(self):
# 'outcomes' corresponds to C_white(player whose move it is, game situation)
# and C_black(player whose move it is, game situation) in the puzzle explanation.
self.outcomes = [[[[[[Outcome.NOT_CALCULATED for _ in range(NR_OF_POSITIONS)]
for _ in range(NR_OF_POSITIONS)]
for _ in range(NR_OF_POSITIONS)]
for _ in range(NR_OF_POSITIONS)]
for _ in range(NR_OF_POSITIONS)]
for _ in range(2)]
self.black_position = 0 # Initialize with a default value
self.white_positions = [0] * 4
self.total = 0
self.initialize_piece_positions()
def initialize_piece_positions(self):
self.black_position = 84
self.white_positions = [11, 13, 15, 17]
def show_progress(self):
self.total += 1
if self.total % 10000 == 0:
print(self.total, end='\r')
def is_usable_field(self, field_number):
if field_number < 10 or field_number >= 90 or field_number % 10 == 0 or field_number % 10 == 9:
# The field is on the outer ring
return False
else:
# Check if in this row the usable fields have odd numbers (this holds for rows 2, 4, 6, and 8)
odd_row = (field_number // 10) % 2 == 1
return (odd_row and field_number % 2 == 1) or (not odd_row and field_number % 2 == 0)
def is_move_allowed(self, piece_position: int, move: int) -> bool:
new_piece_position = piece_position + move
if not self.is_usable_field(new_piece_position):
# A move outside the chessboard is not allowed
return False
if new_piece_position == self.black_position:
# Black is on the new field, therefore the move is not allowed
return False
for white in range(4):
if new_piece_position == self.white_positions[white]:
# A white piece is on the new field, therefore the move is not allowed
return False
return True
def black_has_won(self) -> bool:
return self.black_position in [11, 13, 15, 17]
def white_has_won(self) -> bool:
# White wins if it blocks black in such a way that black cannot make any move anymore
return not (self.is_move_allowed(self.black_position, POSSIBLE_MOVES[0]) or
self.is_move_allowed(self.black_position, POSSIBLE_MOVES[1]) or
self.is_move_allowed(self.black_position, POSSIBLE_MOVES[2]) or
self.is_move_allowed(self.black_position, POSSIBLE_MOVES[3]))
def white_can_make_a_move(self) -> bool:
return (self.is_move_allowed(self.white_positions[0], POSSIBLE_MOVES[0]) or
self.is_move_allowed(self.white_positions[0], POSSIBLE_MOVES[1]) or
self.is_move_allowed(self.white_positions[1], POSSIBLE_MOVES[0]) or
self.is_move_allowed(self.white_positions[1], POSSIBLE_MOVES[1]) or
self.is_move_allowed(self.white_positions[2], POSSIBLE_MOVES[0]) or
self.is_move_allowed(self.white_positions[2], POSSIBLE_MOVES[1]) or
self.is_move_allowed(self.white_positions[3], POSSIBLE_MOVES[0]) or
self.is_move_allowed(self.white_positions[3], POSSIBLE_MOVES[1]))
def get_outcome(self, player: Player, black_position: int, white_positions: list) -> Outcome:
# Because the white pieces are interchangeable, we can sort their positions for more efficient calculation
sorted_white_positions = sorted(white_positions)
return self.outcomes[player.value][
MAPPING[black_position]][
MAPPING[sorted_white_positions[0]]][
MAPPING[sorted_white_positions[1]]][
MAPPING[sorted_white_positions[2]]][
MAPPING[sorted_white_positions[3]]]
def set_outcome(self, player: Player, black_position: int, white_positions: list, outcome: Outcome):
# Because the white pieces are interchangeable, we can sort their positions for more efficient calculation
sorted_white_positions = sorted(white_positions)
self.outcomes[player.value][
MAPPING[black_position]][
MAPPING[sorted_white_positions[0]]][
MAPPING[sorted_white_positions[1]]][
MAPPING[sorted_white_positions[2]]][
MAPPING[sorted_white_positions[3]]] = outcome
def white_move(self):
self.show_progress()
white_can_win = False
black_can_win = False
if self.white_can_make_a_move():
for piece in range(4):
for move in range(2):
if self.is_move_allowed(self.white_positions[piece], POSSIBLE_MOVES[move]):
# Perform the white move
tmp_position = self.white_positions[piece]
self.white_positions[piece] += POSSIBLE_MOVES[move]
# Now black is to move
outcome = self.get_outcome(
Player.BLACK, self.black_position, self.white_positions)
if outcome == Outcome.NOT_CALCULATED:
# This game situation has not been calculated yet
if self.white_has_won():
self.set_outcome(Player.BLACK, self.black_position,
self.white_positions, Outcome.WHITE_CAN_ALWAYS_WIN)
else:
self.black_move()
outcome = self.get_outcome(
Player.BLACK, self.black_position, self.white_positions)
if outcome == Outcome.WHITE_CAN_ALWAYS_WIN:
white_can_win = True
elif outcome == Outcome.BLACK_CAN_ALWAYS_WIN:
black_can_win = True
# Restore the game situation
self.white_positions[piece] = tmp_position
else:
# If white cannot move, it must skip a turn
# Now black is to move
outcome = self.get_outcome(
Player.BLACK, self.black_position, self.white_positions)
if outcome == Outcome.NOT_CALCULATED:
# Prevent endless loops
self.set_outcome(Player.BLACK, self.black_position,
self.white_positions, Outcome.BEING_CALCULATED)
self.black_move()
outcome = self.get_outcome(
Player.BLACK, self.black_position, self.white_positions)
if outcome == Outcome.WHITE_CAN_ALWAYS_WIN:
white_can_win = True
elif outcome == Outcome.BLACK_CAN_ALWAYS_WIN:
black_can_win = True
if white_can_win:
self.set_outcome(Player.WHITE, self.black_position, self.white_positions,
Outcome.WHITE_CAN_ALWAYS_WIN)
elif black_can_win:
self.set_outcome(Player.WHITE, self.black_position, self.white_positions,
Outcome.BLACK_CAN_ALWAYS_WIN)
def black_move(self):
self.show_progress()
black_can_win = False
white_can_win = False
for move in range(4):
if self.is_move_allowed(self.black_position, POSSIBLE_MOVES[move]):
# Perform the black move
tmp_position = self.black_position
self.black_position += POSSIBLE_MOVES[move]
# Now white is to move
outcome = self.get_outcome(
Player.WHITE, self.black_position, self.white_positions)
if outcome == Outcome.NOT_CALCULATED:
# This game situation has not been calculated yet
if self.black_has_won():
self.set_outcome(Player.WHITE, self.black_position,
self.white_positions, Outcome.BLACK_CAN_ALWAYS_WIN)
else:
self.white_move()
outcome = self.get_outcome(
Player.WHITE, self.black_position, self.white_positions)
if outcome == Outcome.BLACK_CAN_ALWAYS_WIN:
black_can_win = True
elif outcome == Outcome.WHITE_CAN_ALWAYS_WIN:
white_can_win = True
# Restore the game situation
self.black_position = tmp_position
if black_can_win:
self.set_outcome(Player.BLACK, self.black_position, self.white_positions,
Outcome.BLACK_CAN_ALWAYS_WIN)
elif white_can_win:
self.set_outcome(Player.BLACK, self.black_position, self.white_positions,
Outcome.WHITE_CAN_ALWAYS_WIN)
print("Creating game object...")
game = CatAndMouseGame()
print("Running game...")
game.black_move()
print(game.total)
game.initialize_piece_positions()
print(game.get_outcome(Player.BLACK, game.black_position, game.white_positions).name)
