Dzisiejszy wpis omawia grę Sliding Puzzle. Gra ta ma proste reguły. Polega ona na tym, aby wykorzystać lukę do przesuwania puzzli, tak aby uzyskać liczby w rosnącej kolejności.
Dla przykładu, gra może mieć na początku następujący stan:

W tym przypadku możemy wykonać 3 możliwe ruchy:
- przesunąć 6 w dół,
- przesunąć 3 w górę,
- przesunąć 2 w prawo.
Gdy wybierzemy 3, wtedy plansza będzie wyglądać następująco:

Teraz możemy wykonać ruch 3 w dół albo 8 w prawo.
Implementacja
Lista list bardzo dobrze nadaje się do reprezentowania planszy:
board = [
[1, 0, 2],
[3, 4, 5],
[6, 7, 8]
]Zero oznacza puste pole, od którego wychodzimy podczas rozgrywki.
Do sprawdzenia, czy dwie pozycje są sąsiednie, możemy wykorzystać funkcję connected, która zwraca wartość logiczną:
def connected(a, b):
row_diff = abs(a[0] - b[0])
col_diff = abs(a[1] - b[1])
return row_diff + col_diff == 1
Funkcja ta liczy dystans między pozycjami i jeśli uzyskana wartość wynosi 1, wtedy dwie pozycje są sąsiednie.
Operowanie samymi współrzędnymi pozycji nie jest wygodne. W tej sytuacji znacznie łatwiej jest odwołać się do określonej liczby, np. 3, zamiast do jej pozycji, która obecnie wynosi [1, 2]. Takie odwołanie staje się możliwe dzięki funkcji get_pos:
def get_pos(board, x):
for row_index, row in enumerate(board):
for col_index, value in enumerate(row):
if value == x:
return row_index, col_index
return NonePowyższa funkcja przyjmuje planszę i szukaną wartość, a jako wynik zwraca jej pozycję. Teraz możemy napisać kod, który oceni, czy obie wartości zajmują sąsiednie miejsca na planszy:
print(connected(
get_pos(board, 1),
get_pos(board, 0)
))Po zweryfikowaniu czy pozycje są sąsiadujące, czas najwyższy, by zapisać funkcję, która dokona zamiany wartości na wskazanych polach.
def swap(board, a, b):
v1 = board[a[0]][a[1]]
v2 = board[b[0]][b[1]]
board[a[0]][a[1]] = v2
board[b[0]][b[1]] = v1Funkcja swap przyjmuje planszę i dwie dowolne pozycje, w obrębie których ma dojść do zamiany wartości.
Pozostaje jeszcze obsługa użytkownika. Chociaż gra jest prosta, to na etapie wprowadzania wartości, można popełnić różne błędy, które warto obsłużyć w tworzonym programie.
def ask_user(board):
zero_pos = get_pos(board, 0)
while True:
try:
nr = int(input('> '))
except ValueError:
print('number is required')
continue
user_pos = get_pos(board, nr)
if user_pos is None:
print('required number between 1 and 8')
continue
if not connected(zero_pos, user_pos):
print('wrong number, try again')
continue
return user_pos
Funkcja ask_user jest odporna na podanie znaków różnych od cyfr. Sprawdza dostępność pozycji na planszy i na sam koniec weryfikuje, czy wybrane pola są sąsiednie względem siebie.
Do sprawdzenia, czy gra dobiegła końca, możemy wykorzystać dwie następujące funkcje:
def iter_board(board):
for row in board:
for value in row:
yield value
def is_ordered(board):
values = list(iter_board(board))
return sorted(values) == values
Podczas gdy funkcja is_ordered odpowiada na pytanie, czy pola są ułożone rosnąco, funkcja iter_board tworzy generator, który umożliwia przejście po dwuwymiarowej strukturze. To pozwala na wykorzystanie funkcji sorted.
Na sam koniec pozostała nam jeszcze funkcja run game, która wykorzystuje wcześniej omówione funkcje.
def run_game():
board = [
[1, 0, 2],
[3, 4, 5],
[6, 7, 8]
]
while True:
for row in board:
print(row)
user_pos = ask_user(board)
zero_pos = get_pos(board, 0)
swap(board, user_pos, zero_pos)
if is_ordered(board):
break
for row in board:
print(row)
run_game()Szczegółowy opis każdej z funkcji znajdziesz również na moim kanale youtube. Linki do odcinków znajdują się poniżej: