Skip to content

Commit db42c25

Browse files
committed
Add slider_puzzle with tests
1 parent 44ce279 commit db42c25

3 files changed

Lines changed: 304 additions & 0 deletions

File tree

.coverage

52 KB
Binary file not shown.

Schieberaetsel/slider_puzzle.py

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import random
2+
3+
SIZE = 4 # 4x4 Feld
4+
5+
6+
def print_board(board):
7+
"""Zeigt das Spielfeld 4x4 in der Konsole an."""
8+
for row in range(SIZE):
9+
line = []
10+
for col in range(SIZE):
11+
value = board[row * SIZE + col]
12+
if value == 0:
13+
line.append(" . ")
14+
else:
15+
line.append(f"{value:2d}")
16+
print(" ".join(line))
17+
print()
18+
19+
20+
def is_solved(board):
21+
"""Prüft, ob das Puzzle gelöst ist: 1..15 und 0 (leer) am Ende."""
22+
return board == list(range(1, 16)) + [0]
23+
24+
25+
def inversion_count(board):
26+
"""Zählt die Inversionen (für Lösbarkeitsprüfung)."""
27+
flat = [x for x in board if x != 0] # 0 (leer) ignorieren
28+
inv = 0
29+
for i in range(len(flat)):
30+
for j in range(i + 1, len(flat)):
31+
if flat[i] > flat[j]:
32+
inv += 1
33+
return inv
34+
35+
36+
def is_solvable(board):
37+
"""
38+
Prüft, ob ein 4x4-Puzzle lösbar ist.
39+
Regel für gerade Breite (4):
40+
- Board-Breite gerade
41+
- Zähl Inversionen (ohne 0)
42+
- Bestimme Zeile des leeren Felds von unten (1..4)
43+
- Wenn diese Zeile von unten ungerade: Inversionen müssen gerade sein
44+
- Wenn diese Zeile von unten gerade: Inversionen müssen ungerade sein
45+
"""
46+
inv = inversion_count(board)
47+
zero_index = board.index(0)
48+
row_from_top = zero_index // SIZE
49+
row_from_bottom = SIZE - row_from_top # 1..4
50+
51+
if SIZE % 2 == 0: # 4 ist gerade
52+
if row_from_bottom % 2 == 1:
53+
# leere Kachel in Zeile 1,3 von unten -> Inversionen gerade
54+
return inv % 2 == 0
55+
else:
56+
# leere Kachel in Zeile 2,4 von unten -> Inversionen ungerade
57+
return inv % 2 == 1
58+
else:
59+
# Für ungerade Breite: Inversionen müssen gerade sein
60+
return inv % 2 == 0
61+
62+
63+
def generate_shuffled_board():
64+
"""Erzeugt ein zufällig gemischtes, aber lösbares Board, das nicht direkt gelöst ist."""
65+
board = list(range(1, 16)) + [0]
66+
while True:
67+
random.shuffle(board)
68+
if is_solvable(board) and not is_solved(board):
69+
return board
70+
71+
72+
def can_move(board, from_index, to_index):
73+
"""Prüft, ob die Tile an from_index auf die leere Tile an to_index verschoben werden darf."""
74+
if to_index < 0 or to_index >= len(board):
75+
return False
76+
fr_row, fr_col = divmod(from_index, SIZE)
77+
to_row, to_col = divmod(to_index, SIZE)
78+
# Nachbarschaft in 4er-Manhattan-Distanz
79+
return abs(fr_row - to_row) + abs(fr_col - to_col) == 1
80+
81+
82+
def move_tile(board, direction):
83+
"""
84+
Versucht, das LEERFELD (0) mit w/a/s/d zu bewegen.
85+
86+
w = leer nach oben (tauscht mit Feld darüber)
87+
s = leer nach unten (tauscht mit Feld darunter)
88+
a = leer nach links (tauscht mit Feld links daneben)
89+
d = leer nach rechts (tauscht mit Feld rechts daneben)
90+
91+
Wenn das Leerfeld am Rand ist (oben/links/rechts/unten) und
92+
man in diese Richtung drückt, passiert nichts (False).
93+
"""
94+
zero_index = board.index(0)
95+
row, col = divmod(zero_index, SIZE)
96+
97+
if direction == "w":
98+
# oben geht nicht weiter hoch
99+
if row == 0:
100+
return False
101+
target_row, target_col = row - 1, col
102+
103+
elif direction == "s":
104+
# unten geht nicht weiter runter
105+
if row == SIZE - 1:
106+
return False
107+
target_row, target_col = row + 1, col
108+
109+
elif direction == "a":
110+
# links geht nicht weiter nach links
111+
if col == 0:
112+
return False
113+
target_row, target_col = row, col - 1
114+
115+
elif direction == "d":
116+
# rechts geht nicht weiter nach rechts
117+
if col == SIZE - 1:
118+
return False
119+
target_row, target_col = row, col + 1
120+
121+
else:
122+
# ungültige Taste
123+
return False
124+
125+
target_index = target_row * SIZE + target_col
126+
127+
# Hier sind wir sicher im Grid und der Nachbar liegt direkt an
128+
board[zero_index], board[target_index] = board[target_index], board[zero_index]
129+
return True
130+
131+
132+
def main():
133+
print("4x4 Schieberätsel (15-Puzzle)")
134+
print("Ziel: 1-15 sortiert, leeres Feld unten rechts.\n")
135+
print("Steuerung: w = hoch, s = runter, a = links, d = rechts, q = beenden\n")
136+
137+
board = generate_shuffled_board()
138+
moves = 0
139+
140+
while True:
141+
print_board(board)
142+
if is_solved(board):
143+
print(f"Glückwunsch! Du hast das Puzzle in {moves} Zügen gelöst!")
144+
break
145+
146+
cmd = input("Zug (w/a/s/d/q): ").strip().lower()
147+
if cmd == "q":
148+
print("Spiel beendet.")
149+
break
150+
151+
if cmd in ("w", "a", "s", "d"):
152+
if move_tile(board, cmd):
153+
moves += 1
154+
else:
155+
print("Ungültiger Zug!")
156+
else:
157+
print("Bitte w, a, s, d oder q eingeben.")
158+
159+
160+
# ✨ Ohne diesen Block startet das Programm NICHT!
161+
if __name__ == "__main__":
162+
main()
163+
else:
164+
print("TEST")
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# test_slider_puzzle.py
2+
import slider_puzzle as sp
3+
4+
5+
def test_is_solved_true_for_solved_board():
6+
board = list(range(1, 16)) + [0]
7+
assert sp.is_solved(board) is True
8+
9+
10+
def test_is_solved_false_for_unsolved_board():
11+
board = [1,2,3,4,5,6,7,8,9,10,11,12,13,15,14,0]
12+
assert sp.is_solved(board) is False
13+
14+
15+
def test_inversion_count_solved_board_zero():
16+
board = list(range(1, 16)) + [0]
17+
assert sp.inversion_count(board) == 0
18+
19+
20+
def test_inversion_count_simple_inversion():
21+
board = [1,2,3,4,5,6,7,8,9,10,11,12,13,15,14,0]
22+
assert sp.inversion_count(board) == 1
23+
24+
25+
def test_is_solvable_for_solved_board():
26+
board = list(range(1, 16)) + [0]
27+
assert sp.is_solvable(board) is True
28+
29+
30+
def test_is_solvable_false_for_known_unsolvable():
31+
board = [1,2,3,4,5,6,7,8,9,10,11,12,13,15,14,0]
32+
assert sp.is_solvable(board) is False
33+
34+
35+
def test_generate_shuffled_board_properties():
36+
board = sp.generate_shuffled_board()
37+
assert len(board) == 16
38+
assert sorted(board) == list(range(16)) # 0..15
39+
assert sp.is_solvable(board) is True
40+
assert sp.is_solved(board) is False
41+
42+
43+
def test_can_move_neighbors_true():
44+
board = [1,2,3,4,
45+
5,6,7,8,
46+
9,10,0,11,
47+
13,14,15,12]
48+
# leeres Feld Index 10
49+
assert sp.can_move(board, 9, 10) is True
50+
assert sp.can_move(board, 11, 10) is True
51+
assert sp.can_move(board, 6, 10) is True
52+
assert sp.can_move(board, 14, 10) is True
53+
54+
55+
def test_can_move_diagonal_false():
56+
board = [1,2,3,4,
57+
5,6,7,8,
58+
9,10,0,11,
59+
13,14,15,12]
60+
assert sp.can_move(board, 5, 10) is False # diagonal
61+
62+
63+
def test_can_move_wrap_around_false():
64+
board = [1,2,3,4,
65+
5,6,7,8,
66+
9,10,0,11,
67+
13,14,15,12]
68+
assert sp.can_move(board, 7, 8) is False
69+
70+
71+
def test_move_tile_valid_and_board_changes():
72+
board = [1,2,3,4,
73+
5,6,7,8,
74+
9,10,0,11,
75+
13,14,15,12]
76+
zero_before = board.index(0)
77+
moved = sp.move_tile(board, "a") # versucht, 0 nach rechts zu bewegen
78+
assert moved is True
79+
assert board.index(0) != zero_before
80+
81+
82+
def test_move_tile_invalid_direction():
83+
board = list(range(1, 16)) + [0]
84+
copy = board.copy()
85+
moved = sp.move_tile(board, "x")
86+
assert moved is False
87+
assert board == copy
88+
89+
def test_move_tile_blocked_at_edges():
90+
# Leerfeld oben links: 'w' und 'a' dürfen nichts tun
91+
board_top_left = [
92+
0, 2, 3, 4,
93+
5, 6, 7, 8,
94+
9, 10, 11, 12,
95+
13,14, 15, 1
96+
]
97+
copy_top_left = board_top_left.copy()
98+
assert sp.move_tile(board_top_left, "w") is False
99+
assert board_top_left == copy_top_left
100+
assert sp.move_tile(board_top_left, "a") is False
101+
assert board_top_left == copy_top_left
102+
103+
# Leerfeld oben rechts: 'w' und 'd' dürfen nichts tun
104+
board_top_right = [
105+
1, 2, 3, 0,
106+
5, 6, 7, 8,
107+
9, 10, 11, 12,
108+
13,14, 15, 4
109+
]
110+
copy_top_right = board_top_right.copy()
111+
assert sp.move_tile(board_top_right, "w") is False
112+
assert board_top_right == copy_top_right
113+
assert sp.move_tile(board_top_right, "d") is False
114+
assert board_top_right == copy_top_right
115+
116+
# Leerfeld unten links: 's' und 'a' dürfen nichts tun
117+
board_bottom_left = [
118+
1, 2, 3, 4,
119+
5, 6, 7, 8,
120+
9, 10, 11, 12,
121+
0, 14, 15, 13
122+
]
123+
copy_bottom_left = board_bottom_left.copy()
124+
assert sp.move_tile(board_bottom_left, "s") is False
125+
assert board_bottom_left == copy_bottom_left
126+
assert sp.move_tile(board_bottom_left, "a") is False
127+
assert board_bottom_left == copy_bottom_left
128+
129+
# Leerfeld unten rechts: 's' und 'd' dürfen nichts tun
130+
board_bottom_right = [
131+
1, 2, 3, 4,
132+
5, 6, 7, 8,
133+
9, 10, 11, 12,
134+
13,14, 15, 0
135+
]
136+
copy_bottom_right = board_bottom_right.copy()
137+
assert sp.move_tile(board_bottom_right, "s") is False
138+
assert board_bottom_right == copy_bottom_right
139+
assert sp.move_tile(board_bottom_right, "d") is False
140+
assert board_bottom_right == copy_bottom_right

0 commit comments

Comments
 (0)