Projektvorstellung: Wait Game

Robocopy

Cadet 4th Year
Registriert
Dez. 2021
Beiträge
95
Github-Link:

Projekt in Github

Das Projekt kann hier direkt getestet werden:

Wait Game in Repl.it



Hallo Freunde. Zu Übungszwecken habe ich sowas wie "Cookie Clicker", also ein ziemlich hohles Spiel, für die Kommandozeile geschrieben. Ich interessiere mich für Spiele-Programmierung und Ziel dieses kleinen Projekts war es, ein Aufstiegssystem und Skill-Set sowie ein Item-System zu entwicklen, dessen Werte so skaliert sind, dass das Zusammenwirken der Zahlen bis in alle Ewigkeit einigermaßen Sinn macht.

Eine falsche Skallierung von Belohnungswerten könnte bspw. im späteren Spielverlauf zur Folge haben, dass man ein Level-Up nach dem anderen bekommt, und der Aufstieg / die Verbesserung von Werten zu schnell oder zu langsam geht.

Außerdem ging es natürlich auch darum, ein wenig Programmiern an sich zu üben.

Mich würde einfach mal eure Meinung zum Quelltext interessieren. Gibt's da Dinge, die ich hätte besser machen können?
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: BeBur
Keine Kommentare = keine Lust zum Lesen des Codes.
 
  • Gefällt mir
Reaktionen: Evolutio, Rickmer, Cool Master und eine weitere Person
Ich habs mal kurz überflogen sehr viele if else Anweisungen. Schau mal ob da nicht ein "case" mehr Sinn ergibt.

Ansonsten Kommentare bitte! Du wirst in 4 Wochen nicht mehr wissen was du da gemacht hast.
 
Rickmer schrieb:
Deinen Code kommentieren ;)
Für was gibt es sprechenden Code?

Rassnahr schrieb:
An einigen stellen wird lediglich der Sinn einiger Zeilen Code erklärt, das hätte man auch einfach gemäß dem Clean Code Prinzip mit aussagekräftiger Bezeichnung der Variablen bzw. Auslagerung in weitere Funktionen mit entsprechender Bezeichnung lösen können.

Der Code ist gut, jedoch folgt er nicht strikt den Clean Code Prinzipien.
 
  • Gefällt mir
Reaktionen: larud, jb_alvarado, kuddlmuddl und 2 andere
(ich füge hier immer mal dinge hinzu)
Wie so oft hab ich das wichtigste am Anfang vergessen:
Insgesamt gefällt mir der Code, gute Namen, aufgeräumt. Es ist keine Qual durchzustöbern.

Es ist Java, da hab ich etwas Ahnung:
- NIEMALS Exception mit try fanngen und dann "e.printStackTrace();"

entweder einfach die Methode mit throws deklarieren oder im catch block einne "runtimeexception" werfen. in 99% der Fälle wirfst "IllegalArgumentException" der Rest teilt sich in NullpointerException und IllegalStateException auf.

Ich empfehle prinzipiell bei Java dieses Buch: https://github.com/muhdkhokhar/test/blob/master/Joshua Bloch - Effective Java (3rd) - 2018.pdf

(es ist weniger einn Buch zum lernen als vielmehr zum Nachschlagen - lies z.B. mal das Kapitel zum Exception Handling)

Es ist z.B. völlig ok (best practice) hier anstatt sysout eine exception zu werfen:

Code:
    public static void initConstantsFromPropFile(String fileName) {
        System.out.println("not implemented yet");
    }

Dafür gibt es https://docs.oracle.com/javase/7/docs/api/java/lang/UnsupportedOperationException.html
siehe https://docs.oracle.com/javase/7/docs/api/java/lang/UnsupportedOperationException.html

---------------------------------
Bei methodenn die etwas zurückgeben empfehele ich folgendes Pattern:
  • default wert setzen "result"
  • am Ende result zurückgeben
  • dazwischen if und else und logik welche result änmdern

z.b. aus:
Code:
public boolean payGold(int value) {
        if (value <= gold) {
            gold -= value;
            return true;
        }
 
        return false;
    }

wird

Code:
public boolean payGold(int value) {
        boolean result = false;
        if (value <= gold) {
            gold -= value;
            result = true;
        }
 
        return result;
    }

Das macht auf den ersten Blick nicht so viel Unterschied, aber mit der Zeit schon - die Wartung solcher Methoden ist viel einfacher. Dafür gibt es auch Qualitätsmetriken iwe "zyklomatische" oder "cognitive" Komplexität. Die wird z.B. schlechter wenn man mehrere returns hat.

--------------------------------------------
zum Tooling: - advanced Level

Build-Tool
es ist zu empfehlen ein Build-Tool zu verwenden (Maven oder Gradle) ohne soetwas ist es schlicht nicht professionell in der Javawelt. Daraus ergibt sich auch eine ganz bestimmte Verzeichnisstzruktur.

Es ist gut dass du hier und dort toString implementierst. Du solltest auch "equals" und "hashcode" implementieren ABER nicht selbst schreiben: die IDEs (IntelliJ oder Eclipse und andere) können alle drei Methoden generieren. Besser: verwende "Lombok" das generiert dir im Hintergrund diese Methoden und auch andere - das ist aber schon sehr advanced.

--------------------------------
Klassen die nur statische Methoden enthalten sind immer etwas verdächtig. Ich hab mir Tools.java nicht genau angeschaut, aber prüfe diese Dinge:
  • die Gruppierung der Methoden (Name Tools ist blöd) - also mehrere Klassen "StringTools", "FileTools"
  • kann man bestimmte Dinge als Methode(n) in Klassen bereitstellen (das zu sehen ist nicht leicht)
-------------------------------
anstatt "programm starten.txt" würde ich auf die oberste ebene eine "readme.md" anlegen und die Anleitung dort reinschreiben - "md" steht für markdown, damit kannst du sehr schöne Anleitung einfach schrieben die direkt in github angezeigt werden.
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: niteaholic, netzgestaltung, Ganymoe und 4 andere
Ich kann dir nur das Buch Clean Code von Robert C. Martin wärmstens empfehlen!

Die folgenden Punkte hat @dermoritz auch schon angemerkt... (Habs aber nach dem schreiben erst so richtig bemerkt ;) )

Weiters solltest du dir mal ein paar externe Libraries ansehen und verwenden.
z.B: https://projectlombok.org/

Hier kannst du einiges an code vermindern und den Schreibaufwand drastisch kürzen!

aus
Code:
// aus folgendem Code  

    private int patBonus;

    public int getPatBonus() {
        return patBonus;
    }
   
    public void setPatBonus(int patBonus) {
        this.patBonus = patBonus;
    }

// wird mit Lombok dieser:

    @Setter  // lombok erstellt dir damit die setPatBonus und du kannst genau so verwenden wie oben
    @Getter // selbes für die getPatBonus
    private int patBonus;

Aber da musst du dir ein paar tutorials dazu ansehen. (ww.baeldung.com)
Diese Annotations gibts dann auch für Konstruktoren (FullArgsConstructor, etc..)


In dem Zuge (von externen libs) wird dann ein build-tool auch sehr h ilfreich... Ich schlag vor Gradle.
https://gradle.org/

Und eine der wichtigesten Seiten https://www.baeldung.com/gradle (hier gleich mit der "Einschulung" für Gradle ;)

Ansonsten würd ich von zuvielen Kommentaren abraten (nicht wie von vielen hier verlangt...)
  • Benenne deine Methoden so, das es ersichtlich ist, was sie macht. Das sollte reichen.
    -> damit das aber auch funktioniert sollten dein Methoden auch nur das machen.
    ->Keep it simple stupid ( https://de.wikipedia.org/wiki/KISS-Prinzip) mach viele kurze methoden (max 10 Zeilen, wenns geht noch weniger)
  • Bei Klassen/Interfaces etc. kannst du ja eine kurze Beschreibung der Klasse machen.
  • Den Rest würd ich aber - wie ebenfalls schon von jemand anderem angemerkt - in eine Readme.md (oder auch README.adoc find ich persönlich besser) packen. Das hat auch den Vorteil dass du im Repo auch gleich die Doku hast.


Das mit den IF Abzweigungen ist wirklich massiv :)

Wie gesagt. Ich würd dir wirklich empfehlen, dir das Buch zu besorgen. Das hat bei uns in der Firma die Qualität wirklich ordentlich erhöht.
Alles wird lesbarer, verständlicher - für sich selbst und auch für andere.


EDIT:
Ich sehe 0 Tests... und das ist eines der wichtigsten Themen beim Programmieren. ABER, auf die Richtige Reihenfolge kommt es drauf an:

Test driven development!

Wird auch im Buch behandelt, aber google einfach mal danach und les dir das Konzepte dahinter durch.
ganz grob: Du schreibst einen Test, danach den Code, dann erweiterst du den Test und passt den Code an, solang bis alle deine Kriterien erfüllt sind.

Für dieses Konzept ist dann hilfreich einfache Tests zu schreiben, das geht mit Groovy und Spock library sehr gut! -> https://www.baeldung.com/groovy-spock
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: Boa-P, Robocopy, dermoritz und 2 andere
KitKat::new() schrieb:
Für was gibt es sprechenden Code?
Da hast Du nicht unrecht. Ein dumbes schreien nach Kommentaren macht kaum Sinn. Vor allem wenn es dann Kommentare sind die lediglich beschreiben was der Code macht. Weil dafür hat man ja den Code.
Man sollte also, wenn man kommentiert nicht sagen wie man was macht, sondern was man macht.

Ganymoe schrieb:
Test driven development!
Ja. Finde ich aber nicht immer angebracht. Oft weiß ich am Beginn noch gar nicht, wie ich ein Problem konkret lösen will und spiele eher mit dem Code herum und erst nach und nach formt sich dann die Lösung heraus.
In dem Fall hab ich vorher noch nicht wirklich viel, was ich testen kann.
 
  • Gefällt mir
Reaktionen: Ganymoe
andy_m4 schrieb:
Ja. Finde ich aber nicht immer angebracht. Oft weiß ich am Beginn noch gar nicht, wie ich ein Problem konkret lösen will und spiele eher mit dem Code herum und erst nach und nach formt sich dann die Lösung heraus.
In dem Fall hab ich vorher noch nicht wirklich viel, was ich testen kann.

Aber im Grunde kann man es immer anwenden, und die Zeitersparnis - trau ich mir wetten - ist in den meisten Fällen besser wenn ich vorher tests schreibe - bevor ich das feature implementiere.

Denn, was passiert wenn ich keinen Test schreibe... Ich starte das Programm, muss mich zum neuen Feature navigieren, und dann mach ich den Test was ich schreibe einfach manuell. Wenn das dann ein paar mal schief geht - und das wird es wie wir wissen - dann wär ich schon schneller mitm Tests schreiben gewesen.

Wenn ich ein neues Feature in der Arbeit mache, wird zu 90% alles funktionieren und implementiert sein bevor ich das erste mal den Springboot Server starte und es manuell durchteste. Ist vielleicht bei Springboot nicht so relevant, aber bei unseren alten Tomcatserver die beim starten über IntelliJ mal 1 Minute brauchen (deploy und start) macht das einen Unterschied aus.


Vielleicht ist es am Anfange die größere Hürde das ganze mal aufzusetzen - da man ja die Sprache lernen will und schon heiss drauf ist zu coden... ich versteh das :)
Aber ich bin grad dabei mich wieder in C++ einzuarbeiten (es ist seit dem Studium doch schon ein wenig Zeit vergangen 😅) und das erste war, ich hab nach einer TestSuit dafür gesucht.
 
Nur ein paar Hinweise:
  • Binaries werden im repo nicht benötigt. Fressen nur Platz und verwaltet werden können sie auch nicht. Die kann man wenn nötig als Release bereitstellen.
  • Entsprechend .class, .jar und alles, was kompiliert wurde, ersatzlos raus aus dem Repo.
  • der gemeine Github-User weiß, wie er eine JAR-Datei auszuführen hat, besonders wenn es nur eine gibt. Vielleicht ist das im deutschsprachigen Raum anders (keine Ahnung) aber die "Anleitung" ist als solches nicht nötig und riskiert eher, daß man sie als von oben herab wahrnimmt.
  • class Main: was sollen die *Player Methoden da? Bin irritiert. Die sollten vermutlich woanders hin... in eine IO Klasse vielleicht?
  • die Chose mit "notimplemented" wurde ja schon erwähnt: throw new NotImplementedException() oder sonstwas. Der Umstand, daß die Funktionalität fehlt, ist eine Ausnahme. Also darf man diese Ausnahme auch wie eine Ausnahme = aka Exception --- behandeln.

class Constants: Err... no. Sorry. Bitte nochmal mit OO Modellierung auseinandersetzen.
Hier rein fallen sicherlich auch die Main::*Player() Methoden, schätz ich mal. 🤔

class Game: Hmm. Verwechsle ich da was, weil ich fühle mich gerade an Highscoreboards erinnert. Bin ich da richtig?
In jedem Fall ist Game zu eng an die Implementierung gekoppelt. Sowas wie System.in hat hier nix zu suchen -- der Klasse selbst sollte es völlig egal sein, ob die in Konsole oder in GUI verarbeitet wird, aber momentan kann das Teil buchstäblich nur mit der Konsole.

class Item: Wäre prima geeignet als Superklasse für verschiede Items. Aber so wurde sie nicht verwendet.

Beispiel:
Item => Robe => Priest Robe / Bath robe / whatever robe
Item => Band => Gold band / Silver band / whatever band

und so weiter.
Natürlich gibts nicht ewig lange Paramliste für die Klasse "Item". Eine Instanz ist ein Item. Nämlich ein Bademantel, oder ein Gold band, oder eine Priesterrobe, oder oder oder. Nicht alles zusammen.



Ansonsten ist der Code verständlich. Ich schließe mich insoweit erstmal der "vorherrschenden" Meinung an: per aktuellem Stand sind (noch) keine spezifischen Kommentare erforderlich.

Aber das Modell fehlt halt gefühlt ganz. Und dann braucht man auch entweder passende Kommentare oder zumindest ein ER-Diagramm. Sonst wird das nämlich ganz schnell unübersichtlich, wer wie wann womit interagiert.
 
  • Gefällt mir
Reaktionen: Robocopy und BeBur
BeBur schrieb:
Aber das 'was', das steht doch da geschrieben als Code?
Im Code steht in erster Linie, wie etwas gemacht wird.
Simples Beispiel: Irgendeine (mehr oder weniger) komplexe RegExp um eine eMail-Adresse zu validieren. Dann schreibst du natürlich nicht wie Du das machst, sondern was Du machst. Wobei in dem Fall das sogar als lesbaren Code schreiben kannst in dem Du einfach eine Funktion validate-email-address dafür definieren kannst. In Fällen wo das geht, sollte man das auch immer vorziehen. Ansonsten halt als Kommentar.

Eine andere Kommentarklasse ist die Kommentierung z.B. von Funktionen. Wo dann grob drauf steht was die macht, welche Parameter sie erwarten, was sie zurück gibt.

Ganymoe schrieb:
Aber im Grunde kann man es immer anwenden, und die Zeitersparnis - trau ich mir wetten - ist in den meisten Fällen besser wenn ich vorher tests schreibe - bevor ich das feature implementiere.
Das klappt relativ gut, wenn ich ungefähr weiß wohin die Reise geht.
Am Anfang hab ich aber oft noch gar keine konkreten Vorstellungen. Der Code den ich dann schreibe hat auch wenig mit dem gemein wie er später aussieht. Das ist dann eher so ein rapid-prototyping-mode wo man halt explorativ mit Code herumspielt und mal so grob guckt, welche Wege könnte man gehen und welche scheiden schon relativ schnell aus.

Das ist vielleicht ein bisschen wie die Komposition eines Musikstückes. Da schreibt ja auch niemand erst Noten aufs Blatt und arrangiert das Stück fertig bevor er das erste Mal ein Instrument in die Hand nimmt. Sondern da probiert man mit dem Instrument ein wenig herum und dann ergibt sich was und erst darauf basierend macht man alles Andere.

Ganymoe schrieb:
Denn, was passiert wenn ich keinen Test schreibe... Ich starte das Programm, muss mich zum neuen Feature navigieren, und dann mach ich den Test was ich schreibe einfach manuell. Wenn das dann ein paar mal schief geht - und das wird es wie wir wissen - dann wär ich schon schneller mitm Tests schreiben gewesen.
Kommt halt immer drauf an. Ich sag ja auch nicht, das man nie "test-driven" macht. Und neue Features kann man ja ggf. auch erst mal so halb standalone implementieren (je nach Situation klappt das oder halt nicht). Das muss ja nicht gleich im Programm sein.

Ganymoe schrieb:
Vielleicht ist es am Anfange die größere Hürde das ganze mal aufzusetzen - da man ja die Sprache lernen will und schon heiss drauf ist zu coden... ich versteh das
Kommt auch immer darauf an, wie weit am Anfang man steht und obs Vorerfahrungen gibt. Umso mehr man noch am Anfang steht und da mit trivalsten Sachen zu kämpfen hab, umso eher würde ich davon abraten sich den Testkram auch noch mit reinzuziehen.

Ganymoe schrieb:
Aber ich bin grad dabei mich wieder in C++ einzuarbeiten
Der Testumfang ist eh von der Sprache abhängig. Umso Regelemtierter die ist, um so weniger umfangreich müssen die Tests ausfallen. Zumindest mach ich das so das ich so viel wie möglich durch "den Compiler" abfangen will wo ich dann u.U. schon in der IDE "at my fingertips" auf Probleme hingewiesen werde.
Nicht umsonst ist der Testkram bei den sogenannten dynamischen Sprachen so extrem beliebt/wichtig, weil da fast alle Fehler erst während der Laufzeit auffallen (können).
 
  • Gefällt mir
Reaktionen: Ganymoe und KitKat::new()
andy_m4 schrieb:
Der Testumfang ist eh von der Sprache abhängig. Umso Regelemtierter die ist, um so weniger umfangreich müssen die Tests ausfallen.
Ich hab da eine andere Wahrnehmung. Die meisten Fehler, die eine kompilierte bzw. statisch typisierte Sprache mit sich bringt, die treten hauptsächlich während der Entwicklung auf und fallen da auch direkt auf.
Tests helfen mir eher dabei, alles Usecases kontinuierlich zu prüfen und Fehler in der Logik zu finden und zu bestätigen, dass die Funktion (immer noch) das tut, was sie soll.

Ich würde eher sagen, dass der Testumfang lokal davon abhängig ist, wie komplex der Teil des Systems ist.
 
BeBur schrieb:
Die meisten Fehler, die eine kompilierte bzw. statisch typisierte Sprache mit sich bringt, die treten hauptsächlich während der Entwicklung auf und fallen da auch direkt auf.
Genau. Während bei dynamischen Sprachen das eben nicht sofort auffällt. Daher brauchst Du da mehr Tests weil sonst schon triviale Dinge wie z.B. falscher Datentyp nicht auffallen (bzw. es dann erst zur Laufzeit "crasht").

BeBur schrieb:
Tests helfen mir eher dabei, alles Usecases kontinuierlich zu prüfen und Fehler in der Logik zu finden und zu bestätigen, dass die Funktion (immer noch) das tut, was sie soll.
Ja. Nichtsdestotrotz sind die halt weniger umfangreich.

BeBur schrieb:
Ich würde eher sagen, dass der Testumfang lokal davon abhängig ist, wie komplex der Teil des Systems ist.
Sicher.
Das Eine schleicht ja das Andere nicht aus.
 
Ganymoe schrieb:
Test driven development!
Auch wenn das eigentlich garnix mit dem Thema zu tun hat. Aber...
TDD ist nur ein Ansatz unter vielen und nicht besser oder schlechter als andere. Man kann da schnell drauf verfallen, nur für den Test zu programmieren. Wie stellst du sicher, dass der Test vollumfänglich ist (und ebenfalls fehlerfrei!)? Dazu werden weitere Faktoren wie Performance etc. damit eher nachrangig betrachtet.
 
playerthreeone schrieb:
Wie stellst du sicher, dass der Test vollumfänglich ist [...]?
Indem man für alle Anforderungen einen Test schreibt?
playerthreeone schrieb:
Dazu werden weitere Faktoren wie Performance etc. damit eher nachrangig betrachtet.
Eine gewisse Performance ist eine Anforderung? Schreib einen Test dafür, ansonsten ist es wohl premature optimization 😉
 
  • Gefällt mir
Reaktionen: Boa-P, Ganymoe und andy_m4
Ich hab das Gefühl, du vergleichst "möglichst dumm TDD umsetzen" mit "möglichst schlau ohne TDD arbeiten" was nicht so recht sinnvoll ist.
Plus das, was KitKat ja schon schrieb. Was soll eine Funktion (im Falle von Unit Tests) denn noch erfüllen, abgesehen von den Anforderungen die an sie gestellt werden?
 
  • Gefällt mir
Reaktionen: Ganymoe
KitKat::new() schrieb:
Indem man für alle Anforderungen einen Test schreibt?
Finde ich lustig.
Früher haben wir einfach nur Code geschrieben. Da war es immer schwierig das der auch das macht, was er sollte. Also haben wir Tests geschrieben. Dann haben wir gemerkt das wir auch hier aufpassen müssen das der vollumfänglich genug ist. Also Schreiben wir jetzt Anforderungen für die Tests und hoffen das wir bei den Anforderungen dann auch alles richtig gemacht haben oder sollen wir vor den Anforderungen noch ne Spezifikation setzen? Oder was kommt dann da vor? und davor? Und davor?

Man muss aufpassen das man sich bei allen solchen Maßnahmen nicht in irgendeiner Bürokratie verliert und zum eigentlichen arbeiten gar nicht mehr kommt. :-)
 
  • Gefällt mir
Reaktionen: playerthreeone
andy_m4 schrieb:
Also Schreiben wir jetzt Anforderungen für die Tests und hoffen das wir bei den Anforderungen dann auch alles richtig gemacht haben oder sollen wir vor den Anforderungen noch ne Spezifikation setzen? Oder was kommt dann da vor? und davor? Und davor?
Nein, du sollst Tests für die Anforderungen schreiben...
 
  • Gefällt mir
Reaktionen: Boa-P und andy_m4
Zurück
Oben