Python script for estimating tinyhouse config...
  1. """
  2. Count (an upper bound on) the number of tinyhouse chess positions.
  3.  
  4. Board: 4x4 = 16 squares. Ranks 2 and 3 are the 8 "middle" squares.
  5.  
  6. Piece inventory (conserved totals, both colours + banks combined):
  7. 1 white king, 1 black king
  8. 2 "pawn-ish" pieces : each is a pawn, or a promoted pawn (pawn-wazir /
  9. pawn-ferz / pawn-horse)
  10. 2 true wazirs, 2 true ferzes, 2 true horses
  11.  
  12. Placement domains:
  13. king, true wazir/ferz/horse ....... any of the 16 squares (or bank, except kings)
  14. plain pawn ........................ one of the 8 middle squares, or the bank
  15. promoted pawn (pawn-X) ............ any of the 16 squares, never the bank
  16. at most one piece per square
  17.  
  18. Method
  19. ------
  20. Every piece is described by a "slot option": (label, place) where
  21. label = (colour, kind) -- what the piece is and whose it is
  22. place = 'M' (middle 8 squares) | 'F' (free: any of 16) | 'bank'
  23.  
  24. Pieces sharing the same label+place are indistinguishable, so for each group
  25. we enumerate *multisets* of options (combinations_with_replacement).
  26.  
  27. Given a whole configuration with `a` on-board pieces restricted to the middle
  28. squares and `b` on-board pieces free to use all 16, the number of ways to place
  29. them as *distinguishable* pieces is
  30. P(8, a) * P(16 - a, b)
  31. (place the middle-restricted ones first; each one eats a square from the 16).
  32. Dividing by k! for every group of k identical on-board pieces turns that into a
  33. count of distinct positions. Identical pieces sitting in the same bank are
  34. already handled by the multiset enumeration, so they need no divisor.
  35.  
  36. Finally multiply by 2 for whose turn it is.
  37. """
  38.  
  39. from itertools import combinations_with_replacement, product
  40. from collections import Counter
  41. from math import perm, factorial
  42.  
  43. COLOURS = ("W", "B")
  44.  
  45. # ---- option lists for each conserved group -------------------------------
  46.  
  47. # the two pawn-ish pieces
  48. PAWN_OPTIONS = (
  49. [((c, "pawn"), "M") for c in COLOURS] # pawn on board
  50. + [((c, "pawn"), "bank") for c in COLOURS] # pawn in hand
  51. + [((c, "pawn-" + p), "F") # promoted pawn
  52. for c in COLOURS for p in ("wazir", "ferz", "horse")]
  53. )
  54.  
  55.  
  56. def true_piece_options(kind):
  57. return ([((c, kind), "F") for c in COLOURS]
  58. + [((c, kind), "bank") for c in COLOURS])
  59.  
  60.  
  61. WAZIR_OPTIONS = true_piece_options("wazir")
  62. FERZ_OPTIONS = true_piece_options("ferz")
  63. HORSE_OPTIONS = true_piece_options("horse")
  64.  
  65. KINGS = [(("W", "king"), "F"), (("B", "king"), "F")]
  66.  
  67. # ---- enumeration ---------------------------------------------------------
  68.  
  69.  
  70. def count_positions(verbose=False):
  71. groups = [
  72. list(combinations_with_replacement(PAWN_OPTIONS, 2)),
  73. list(combinations_with_replacement(WAZIR_OPTIONS, 2)),
  74. list(combinations_with_replacement(FERZ_OPTIONS, 2)),
  75. list(combinations_with_replacement(HORSE_OPTIONS, 2)),
  76. ]
  77.  
  78. total = 0
  79. by_pieces_on_board = Counter() # how many pieces stand on the board
  80. configs = 0
  81.  
  82. for combo in product(*groups):
  83. pieces = KINGS + [item for grp in combo for item in grp]
  84.  
  85. a = sum(1 for _, place in pieces if place == "M")
  86. b = sum(1 for _, place in pieces if place == "F")
  87.  
  88. placements = perm(8, a) * perm(16 - a, b)
  89. if placements == 0:
  90. continue
  91.  
  92. # divide out permutations of identical on-board pieces
  93. board_counts = Counter(p for p in pieces if p[1] != "bank")
  94. divisor = 1
  95. for k in board_counts.values():
  96. divisor *= factorial(k)
  97.  
  98. n = placements // divisor
  99. total += n
  100. configs += 1
  101. by_pieces_on_board[a + b] += n
  102.  
  103. total *= 2 # side to move
  104.  
  105. if verbose:
  106. print(f"piece configurations considered : {configs}")
  107. print("positions by number of pieces standing on the board "
  108. "(before the x2 for side to move):")
  109. for k in sorted(by_pieces_on_board):
  110. print(f" {k:2d} pieces on board : {by_pieces_on_board[k]:>14,}")
  111. print()
  112. return total
  113.  
  114.  
  115. if __name__ == "__main__":
  116. t = count_positions(verbose=True)
  117. print(f"TOTAL tinyhouse positions (upper bound): {t:,}")
  118. print(f" ~ {t:.4g}")
Pasted 2026-08-24 07:10:41

Short link: