home
erste Version am 27.12.2022
letzte Änderung am 27.12.2022

Tic Tac Toe im Terminal


In den letzten Wochen habe ich mir zum Spielen nach Feierabend immer mal wieder eine Programmieraufgabe bei LeetCode rausgesucht.
Wenn mein Script eigentlich funktioniert hat, aber wegen schlechten Laufzeitverhaltens abgelehnt wurde, habe ich gelegentlich bei NeetCode gespickt - sofern es dort eine Lösung gab.

An meinem Sudoku-Solver habe ich sogar zwei/drei Abende rumgebastelt (das Ding war als Difficulty=Hard deklariert).
Am Schluß hatte mein Script 602 Zeilen und wurde akzeptiert. Kurz danach war ich allerdings etwas frustriert, als ich mir andere Lösungen angesehen hatte.
Meine Lösung ist erst als allerletzten Ausweg in die Rekursion eingestiegen. Alle einfachen Fälle wurden vorab anderweitig abgearbeitet. So quasi mit O(N).
Alle anderen gesichteten Lösungen waren direkt rekusiv ... einfach stumpfes Durchprobieren. Offenbar aber trotzdem schnell genug. Doof. Hatte ich nicht erwartet.
Aber egal.

Die jüngste Programmieraufgabe habe ich mir selbst gestellt. Zwar gibt es bei LeetCode Aufgaben bzgl. TicTacToe, aber die 1275 ist trivial und die 348 ist immerhin als medium deklariert, jedoch bekommt man die nur als zahlender Kunde zu sehen. Hier könnte man sie zwar sehen...aber wenige Sekunden von dem Video reichen mir, um da wohl keinen Spaß dran zu haben.

Laut dem Film War Games geht das Spiel immer unentschieden aus, wenn beide Spieler keine Fehler machen. Das will ich!
Also ein Spiel, gegen das man bestenfalls ein
Unentschieden erreichen kann. Und es soll auch gegen sich selbst spielen können.
Aber ohne TensorFlow o.ä. - dafür braucht es keine KI.
Und unsere heutige KI wird wohl noch weit davon entfernt sein, die Sinnlosigkeit dieses Spiels zu erkennen .... um daraus schließlich abzuleiten, dass ein weltweiter Atomkrieg ebenso sinnlos ist....
Der Film war unterhaltsam, hatte sogar eine klare Botschaft (lasst Computer niemals die Entscheidung über das Abfeuern von ICBMs treffen), das Ende der Story war jedoch Quatsch.



Hier der Python-Code:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# TicTacToe.py
#
# Detlev Ahlgrimm 23.12.2022
#
# 27.12.2022 D.A. Spieler wählbar

import sys
import time
import random

KEYS = [ "7", "8", "9", "4", "5", "6", "1", "2", "3", "q" ]
TRANSPOSE = { "7": 0, "8": 1, "9": 2,
"4": 3, "5": 4, "6": 5,
"1": 6, "2": 7, "3": 8 }
WON = [ (0, 1, 2), (3, 4, 5), (6, 7, 8),
(0, 3, 6), (1, 4, 7), (2, 5, 8),
(0, 4, 8), (2, 4, 6), ]
SWAP = { "X": "O", "O": "X" }
board = [ " ", " ", " ", " ", " ", " ", " ", " ", " " ]

# ----------------------------------------------------------------------
# Zeigt ein Spielfeld mit den jeweiligen Tasten an.
def show_keys():
for y in (0, 1, 2):
for x in (0, 1, 2):
if x < 2:
print(" %s |"%(KEYS[y*3 + x],), end="")
else:
print(" %s"%(KEYS[y*3 + x],))
if y < 2:
print("---+---+---")
print()


# ----------------------------------------------------------------------
# Liefert den gewählten Spielmode.
def get_mode():
print("0 = Mensch/Mensch")
print("1 = Maschine/Mensch")
print("2 = Mensch/Maschine")
print("3 = Maschine/Maschine")
st = input("Spieltyp [0, 1, (2), 3]:")
if st == "" or st not in ("0", "1", "2", "3"):
st = "2"
return int(st)


# ----------------------------------------------------------------------
# Zeigt das aktuelle Spielfeld an.
def print_game():
print()
for y in (0, 1, 2):
for x in (0, 1, 2):
if x < 2:
print(" %s |"%(board[y*3 + x],), end="")
else:
print(" %s"%(board[y*3 + x],))
if y < 2:
print("---+---+---")


# ----------------------------------------------------------------------
# Liefert "X" oder "O", wenn der jeweilige Spieler gewonnen hat. Sonst
# wird None geliefert.
def is_won():
found = False
for t in WON:
s = ""
for p in t:
y, x = p // 3, p % 3
if board[y*3 + x] == " ":
break
s += board[y*3 + x]
if len(s) != 3:
continue
if s[0] == s[1] and s[1] == s[2]:
return s[0]
return


# ----------------------------------------------------------------------
# Liefert eine Liste mit den Index-Werten von unbelegten Feldern.
def get_free():
free = []
for i in range(9):
if board[i] == " ":
free.append(i)
return free


# ----------------------------------------------------------------------
# Liefert den nächsten Zug von Spieler "player" als Index.
def make_move(player, computer):
best_rank = None
best_move = None
free = get_free()
if len(free) == 9: # wenn der computer anfängt...
return random.randint(0, 8) # ...zufälliges Feld liefern
for i in free:
board[i] = player
rank = find_rank(SWAP[player], computer)
board[i] = " "
if best_rank is None or rank > best_rank:
best_rank = rank
best_move = i
return best_move


# ----------------------------------------------------------------------
# Liefert eine Bewertung für die aktuelle Spiel-Stellung aus Sicht von
# Spieler "computer".
def find_rank(player, computer):
free = get_free()
iw = is_won()
if iw == computer:
return 1
elif iw == SWAP[computer]:
return -1
if len(free) == 0:
return 0
ranks = []
for i in free:
board[i] = player
ranks.append(find_rank(SWAP[player], computer))
board[i] = " "
if player == computer:
return max(ranks)
return min(ranks)


# ----------------------------------------------------------------------
#
if __name__ == '__main__':
show_keys()
st = get_mode()
current_player = "X"
if st == 1:
computer = "X"
elif st == 2:
computer = "O"
was_quit = False

while " " in board: # solange es noch freie Felder gibt
print_game()
t = is_won() # auf Ende durch Sieg testen
if t is not None:
print("\033[7A")
break

if st == 0 or (st in (1, 2) and current_player != computer):
print(" "*20, "\033[A") # Zeile löschen
p = input("Feld [1-9]:")
if not p in KEYS:
print("falsche Taste!")
continue
if p == "q":
was_quit = True
print("\033[8A") # acht Zeilen hoch
break
p = TRANSPOSE[p]
elif st == 3:
print()
p = make_move(current_player, current_player)
time.sleep(0.5)
elif st in (1, 2) and current_player == computer:
p = make_move(current_player, computer)
print()

y, x = p // 3, p % 3
if board[y*3 + x] == " ":
board[y*3 + x] = current_player
else:
print("Das Feld ist schon belegt!")
continue

current_player = SWAP[current_player]
print("\033[8A") # acht Zeilen hoch

print_game()
t = is_won()
if t is None and not was_quit:
print("unentschieden!")
elif t is not None:
if t == "X":
print("X hat gewonnen!")
else:
print("O hat gewonnen!")
print()

download als ZIP

Im Terminal sieht es dann beispielweise so aus:
dede@c12:~> ./TicTacToeV2.py
 7 | 8 | 9
---+---+---
 4 | 5 | 6
---+---+---
 1 | 2 | 3

0 = Mensch/Mensch
1 = Maschine/Mensch
2 = Mensch/Maschine
3 = Maschine/Maschine
Spieltyp [0, 1, (2), 3]:3

 X | X | O
---+---+---
 O | O | X
---+---+---
 X | O | X
unentschieden!

dede@c12:~>

Oder so, wenn man nicht aufpasst:
dede@c12:~> ./TicTacToeV2.py
 7 | 8 | 9
---+---+---
 4 | 5 | 6
---+---+---
 1 | 2 | 3

0 = Mensch/Mensch
1 = Maschine/Mensch
2 = Mensch/Maschine
3 = Maschine/Maschine
Spieltyp [0, 1, (2), 3]:1

 X |   | O
---+---+---
 X | X | X
---+---+---
 O |   | O
X hat gewonnen!     

dede@c12:~>

Sinnigerweise wird der Ziffernblock zur Eingabe genutzt.