s3nnet am :
Hey Onli,
erst ein mal ein Frohes Neues!
Sehr cool, ich hab dein Skript jetzt mal hergenommen, um meine Skripting-Kenntnisse ein bisschen aufzufrischen.
Ich drück dir die Daumen! Was gabs denn zu gewinnen?
Im Oktober letzten Jahres hatte GNU/Linux.ch einen Programmierwettbewerb für das Spiel Spyfall veranstaltet. Spyfall ist ein Diskussionsspiel, bei dem eine Gruppe von Spielern durch Fragen den Spion in der Gruppe identifizieren müssen. In jeder Spielrunde wissen alle außer dem Spion an welchem zugeteilten Ort sie sind (z.B. in einem Bunker) und stellen einander in einem Zeitlimit Fragen. Die Programmieraufgabe war nun, dafür ein Helferprogramm zu schreiben, das einem der Spieler die Spionrolle zuweist und allen anderen den Ort verrät.
Mich hatte das (nicht nur wegen dem Preis) direkt interessiert, weil ich hier eine Möglichkeit sah Bilder im Terminal einzusetzen. Denn das geht überraschenderweise, wie mir kürzlich erst WezTerm nochmal gezeigt hatte. Ich erkläre im Folgenden meine in Bash implementierte Lösung im Detail.
Die Spiellogik können wir schnell abhandeln. Am Anfang wird die Spielerzahl eingegeben:
while [[ -z $players || 0 == $players ]];do # ask for players and duration echo "Wie viele Spieler nehmen teil? [3-10]" read players done
Genauso wird die Rundenlänge abgefragt:
echo "Wie viele Minuten soll die Runde dauern? [Standard 8]" read duration if [[ -z $duration ]];then duration=8 fi
Mit den Informationen kann nun aus vorher vorbereiteten Arrays mittels Bashs Zufallsfunktion ein Ort und das passende Bild ausgewählt werden:
# Select a place placeindex=$((RANDOM%${#places[@]})) # Zufallszahl mod der Anzahl möglicher Orte, sodass sie nie größer sein kann place=${places[$placeindex]} # Der gewählte Index bestimmt dann den Ort place_image=${images[$placeindex]} # Und die Bilder, siehe unten place_image_ascii=${images_ascii[$placeindex]}
Und um die Spielerrollen zuzuweisen muss nur der Spion ausgewählt werden:
spy=$((RANDOM%players))
Die Spieler werden dann noch einer nach dem anderen über ihre Rolle informiert:
for i in $(seq 0 $((players - 1)));do echo "Hey Spieler $((i+1)), bist du da und alleine? Bitte bestätige mit Enter:" read confirmed if [[ $spy == $i ]];then # ask for players and duration echo "Hey Spieler $((i+1)), du bist diese Runde der Spion. Bitte bestätige mit Enter:" read confirmed else # ask for players and duration # Hierhin kommt nachher noch der Code zum Bilderanzeigen, siehe unten … echo "Hey Spieler $((i+1)), diese Runde spielt in $place. Bitte bestätige mit Enter:" read confirmed fi clear done
Das Skript hilft während des Spiels dabei das Zeitlimit anzuzeigen, was ich recht simpel durch ein sekündliches Herunterzählen des Countdowns umgesetzt habe:
# On start, start the timer countdown=$((duration*60)) echo "Das Spiel startet nun. Ihr habt $duration Minuten!" while [[ $countdown > 0 ]];do sleep 1 countdown=$((countdown - 1)) echo "Noch $countdown Sekunden!" done
Am Ende soll ein Ton abgespielt werden. Das war gar nicht so einfach. Ich wollte erst nur den Beeper im PC tönen lassen, aber der ist oft deaktiviert, war es bei mir im Terminal beispielsweise. Daher versucht das Skript zusätzlich das oft auf Systemen vorhandene Programm speaker-test
einzuspannen, um für einen Moment einen Ton über den Lautsprecher auszugeben:
# Now we signal the end of the game. # First with the bell, but that might be disabled echo -ne '\a' # Now with speaker-test, to use the regular sound system. Note how we kill it quietly thanks to wait if hash speaker-test 2>/dev/null ;then speaker-test -t sine -f 1000 -l 1 > /dev/null & speaker_pid=$! sleep .2 kill -9 $speaker_pid wait $speaker_pid 2>/dev/null fi
Der Test auf hash speaker-test
ist dabei eine der Möglichkeiten um zu testen, ob ein Befehl auf einem System verfügbar ist. Mir ist nicht mehr klar, warum ich diesen Weg und nicht einen anderen wählte, aber er funktioniert.
Das war der relevante Teil der Spielelogik. Er war verpackt in einer Funktion namens main
, sodass ich außenrum noch ein paar Variablen anlegen konnte. Denn die wurden für das eigentlich interessante gebraucht.
Denn wie kann ein solches Bashskript nun Bilder anzeigen? Es stellt sich raus, dass manche Terminals das einfach können. WezTerm beispielsweise bringt eine Befehlkombination wezterm imgcat
mit, die man auf eine Bilddatei loslassen kann, sodass die dann im Terminal angezeigt wird. Das nutzt mein Skript so:
if [[ "$TERM_PROGRAM" == "WezTerm" ]];then temp_file=$(mktemp) echo "$place_image" | base64 -d > "$temp_file" wezterm imgcat "$temp_file" else
Generisch scheint das Stichwort Sixel zu sein. Ein Protokoll, um Bilder an Terminals zu übertragen und von ihnen darstellen zu lassen. Darauf kann man testen, wobei ich mir von lsix
abschaute wie ein solcher Test aussehen kann. Das nutzt mein Skript alternativ:
# We detect for sixel support, partly how lsix does it stty -echo IFS=";?c" read -a REPLY -s -t 1 -d "c" -p $'\e[c' >&2 for code in "${REPLY[@]}"; do if [[ $code == "4" ]]; then hassixel="yup" break fi done hash lsix 2>/dev/null # hacky, but I did not want to fight with the syntax to have the hash inside the [[ if [[ $hassixel == "yup" && $? ]] ;then temp_file=$(mktemp) echo "$place_image" | base64 -d > "$temp_file" lsix "$temp_file" fi
Das funktioniert! Glaube ich, habe ich es doch letztendlich nur mit WezTerm getestet. Doch was hat es mit dem base64 auf sich, wo kommen die Bilder her?
Ich wollte, dass die Bilder nicht separat im Dateisystem liegen müssen, sondern dass ich sie an Ralf mit dem Programmcode in einer Datei schicken kann. Zuerst erstellte ich dafür ganz normale grafische Bilder mit einem KI-Bildergenerator (Bing, wie hier vorgestellt). Doch dann nutzte ich base64
um von der Bilddatei eine Textrepräsentation zu erstellen (es zu serialisieren) und es der Skriptdatei anzuhängen, z.B.so:
base64 Downloads/zoo.jpg >> spyfall.sh
Um diese Serialisierung auch nutzen zu können packte ich sie in eine Variable, indem ich sie mit Anführungszeichen umstellte und den Variablennamen davorstellte, also so:
zoo="…"
Diese Bilder landeten in der main in einem Array:
# Man beachte die wiederholte Nutzung der Anführungszeichen. images=("$cave" "$spacestation" "$desert" "$disco" "$bunker" "$corn" "$antarktis" "$zoo")
Das Array wurde im Spiel zur Auswahl des Bildes genutzt wie oben bei der Spiellogik gezeigt, indem nur die aktive Arrayposition festgelegt wurde.
Aber nicht alle Terminals können grafische Bilder anzeigen. Für die wollte ich auch eine Ausweichlösung haben. Dafür griff ich auf tiv
zurück. Das ist ein Programm, das aus Bildern eine ASCII-Grafik zaubert, samt den Escapesequenzen um sie einzufärben (was in xterm mir übrigens am besten zu funktionieren schien).
Ein Pluspunkt davon: Das Programm kann ich auf meiner Seite laufen lassen, es muss beim Spieler nicht installiert sein. Beim Spieler reicht es völlig, die gespeicherte Ausgabe des Programms auszugeben. Ich musste also nur wieder die Bildrepräsentation dem Skript anhängen:
tiv Downloads/zoo.jpg >> spyfall.sh
Die wieder in einer Variable speichern:
# Man beachte das einzelne Anführungszeichen, um die Escapesequenzen zu bewahren. zoo_ascii='…'
Und wieder ein Array vorbereiten:
images_ascii=("$cave_ascii" "$spacestation_ascii" "$desert_ascii" "$disco_ascii" "$bunker_ascii" "$corn_ascii" "$antarktis_ascii" "$zoo_ascii")
Nun ist es mit einem echo
darstellbar:
else # Our two graphical methods failed, so we fall back to the ascii images echo "$place_image_ascii" fi
Somit läuft das Skript vernünftig in allen Terminals mit Bash.
Das Spiel selbst zu programmieren war nicht schwer. Sicher, man hätte das besser machen können, so war mein Ansatz mit clear
einfach alte Ausgaben zu entfernen oder den Countdown schlicht mit echo untereinander herunterzuzählen nicht ideal. Hier hätte sich ein interaktives Terminalprogramm angeboten, das (wie nano, top, etc) seinen eigenen Platz schafft und alte Ausgaben überschreiben kann. Aber Bilder einzuarbeiten war mir neues genug.
Es hat auch Spaß gemacht, sowas mal wieder in Bash umzusetzen. Damit gute Lösungen zu finden ist mehr noch ein Knobelspiel als sonst und das Ergebnis trotzdem kompakt und gut lesbar. Vorausgesetzt man verirrt sich nicht in den Bilderdefinitionen (die Skriptdatei ist übrigens 2,6 MB groß). Dass meine Implementierung dann auch noch als valider Gewinnspielkandidat akzeptiert wurde hat mich durchaus gefreut.
Wer sich das im ganzen ansehen will, ich habe das Skript als Gist hochgeladen.
Hey Onli,
erst ein mal ein Frohes Neues!
Sehr cool, ich hab dein Skript jetzt mal hergenommen, um meine Skripting-Kenntnisse ein bisschen aufzufrischen.
Ich drück dir die Daumen! Was gabs denn zu gewinnen?
Leider hatte ich bei der Auslosung kein Glück. Die Preise waren ziemlich gut, ein Telefon von volla, ein raspberry pi oder ein led-blinkding für einen USB-Port.
Aber Danke, und frohes neues :)