Schlachtschiff in CSS bauen

Avatar of Daniel Schulz
Daniel Schulz am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Dies ist ein Experiment, um zu sehen, wie weit ich mit reinem CSS in ein interaktives Erlebnis vordringen kann. Welches Projekt wäre besser geeignet als ein Spiel? Schiffe versenken schien eine gute Herausforderung zu sein und ein Schritt über die bisherigen CSS-Spiele hinaus, da es die Komplexität mehrerer Bereiche hat, die mit zwei Spielern interagieren müssen.

Willst du das komplette Spiel sehen?

Repository anzeigen Demo anzeigen

Oh, du willst lernen, wie es funktioniert? Tauchen wir ein.

Ich wusste sofort, dass es viel repetitives HTML und sehr lange CSS-Selektoren geben würde, also habe ich Pug zur HTML-Kompilierung und Less zur CSS-Kompilierung eingerichtet. In diesen Sprachen wird der gesamte Code ab hier geschrieben.

Interaktive Elemente in CSS

Um die Spielmechanik zum Laufen zu bringen, benötigen wir einige interaktive Elemente. Wir werden sie einzeln durchgehen.

HTML-Checkboxes und :checked

Bei Schlachtschiff muss man oft überprüfen, ob ein Feld ein Schiff enthält oder nicht, daher werden wir eine Menge Checkboxen verwenden.

[type*='checkbox'] {
  // inactive style

  &:checked {
    // active style
  }
}

Um Checkboxes zu stylen, müssten wir sie zunächst mit appearance: none; zurücksetzen, was derzeit nur schlecht unterstützt wird und Browser-Präfixe benötigt. Die bessere Lösung hier ist, Hilfselemente hinzuzufügen. <input>-Tags können keine Kinder haben, einschließlich Pseudo-Elementen (auch wenn Chrome sie trotzdem rendert), daher müssen wir dies mit dem benachbarten Geschwisterselektor umgehen.

[type*='checkbox'] {
  position: relative;
  opacity: none;

  + .check-helper {
    position: absolute;
    top: 0;
    left: 0;
    pointer-events: none;
    // further inactive styles
  }

  &:checked {
    + .check-helper {
      // active styles
    }
  }
}

Wenn Sie ein <label> für das Hilfselement verwenden, erweitern Sie auch den Klickbereich der Checkbox auf das Hilfselement, was Ihnen ermöglicht, es freier zu positionieren. Außerdem können Sie mehrere Labels für dieselbe Checkbox verwenden. Mehrere Checkboxes für dasselbe Label werden jedoch nicht unterstützt, da Sie für jede Checkbox dieselbe ID zuweisen müssten.

Ziele

Wir erstellen ein lokales Mehrspieler-Spiel, daher müssen wir das Schlachtfeld eines Spielers vor dem anderen verstecken und benötigen einen Pausenmodus, der es einem Spieler ermöglicht, zu wechseln, ohne einen Blick auf die Schiffe des anderen Spielers zu werfen. Ein Startbildschirm, der die Regeln erklärt, wäre auch schön.

HTML bietet uns bereits die Möglichkeit, auf eine bestimmte ID im Dokument zu verlinken. Mit :target können wir das Element auswählen, zu dem wir gerade gesprungen sind. Das ermöglicht uns, ein Single-Page-Application-ähnliches Verhalten in einem vollständig statischen Dokument zu erstellen (und das, ohne die Zurück-Taste zu beeinträchtigen).

- var screens = ['screen1', 'screen2', 'screen3'];
body
  nav
    each screen in screens
      a(href='#' + screen)

  each screen in screens
    .screen(id=screen)
      p #{screen}
.screen {
  display: none;

  &:target {
    display: block;
  }
}

Sichtbarkeit und Zeigerereignisse

Elemente inaktiv zu rendern geschieht normalerweise mit pointer-events: none;. Das Coole an pointer-events ist, dass man es für Kindelemente umkehren kann. Das lässt nur das ausgewählte Kind klickbar, aber das Elternelement bleibt durchlässig. Das wird später in Kombination mit Checkbox-Hilfselementen nützlich sein.

Dasselbe gilt für visibility: hidden;. Während display: none; und opacity: 0; das Element und alle seine Kinder verschwinden lassen, kann visibility umgekehrt werden.

Beachten Sie, dass eine versteckte Sichtbarkeit auch alle Zeigerereignisse deaktiviert, im Gegensatz zu opacity: 0;, aber im Dokumentenfluss verbleibt, im Gegensatz zu display: none;.

.foo {
  display: none; // invisible and unclickable
  .bar {
    display: block; // invisible and unclickable
  }
}

.foo {
  visibility: hidden; // invisible and unclickable
  .bar {
    visibility: visible; // visible and clickable
  }
}

.foo {
  opacity: 0;
  pointer-evens: none; // invisible and unclickable
  .bar {
    opacity: 1;
    pointer-events: all; // still invisible, but clickable
  }
}
CSS-Regel Umkehrbare Deckkraft Umkehrbare Zeigerereignisse
display: none;
visibility: hidden;
opacity: 0;
pointer-events: none;

Okay, nachdem wir die Strategie für unsere interaktiven Elemente festgelegt haben, widmen wir uns nun der Einrichtung des Spiels selbst.

Einrichtung

Wir haben einige globale statische Variablen und die Größe unserer Schlachtfelder zu definieren, bevor wir tatsächlich beginnen.

@gridSize: 12;
@zSea: 1;
@zShips: 1000;
@zAbove: 2000;
@seaColor: #123;
@enemyColor: #f0a;
@playerColor: #0c8;
@hitColor: #f27;

body {
  --grid-measurements: 70vw;
  @media (min-aspect-ratio: 1/2) {
    --grid-measurements: 35vh;
  }
}

Die Rastergröße ist die Größe des Schlachtfelds: in diesem Fall 12×12 Felder. Als Nächstes definieren wir einige z-Indizes und Farben.

Hier ist das Pug-Skelett

doctype html

head
  title Ships!
  link(rel="stylesheet", href="style.css")
  meta(charset="UTF-8")
  meta(name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no")
  meta(name="theme-color" content="#000000")

body

Alles HTML ab dieser Stelle befindet sich im body.

Implementierung der Zustände

Wir müssen die Zustände für Spieler 1, Spieler 2, Pause und einen Startbildschirm erstellen. Dies tun wir, wie oben mit Target-Selektoren erklärt. Hier ist eine kleine Skizze dessen, was wir erreichen wollen.

Wir haben einige Modi, jeder in seinem eigenen Container mit einer ID. Nur ein Modus soll im Viewport angezeigt werden – die anderen sind über display: none; versteckt, mit Ausnahme der Spielermodi. Wenn ein Spieler aktiv ist, muss der andere außerhalb des Viewports sein, aber immer noch Zeigerereignisse haben, damit die Spieler miteinander interagieren können.

.mode#pause

each party in ['p1', 'p2']
  .mode(id=party)

.mode#start

.status
  each party  in ['p1', 'p2']
    a.player-link.switch(href='#' + party)
  a.status-link.playpause(href='#pause') End Turn

h1
  Ships!

Der .status div enthält die Hauptnavigation. Seine Einträge ändern sich je nach aktivem Modus, daher müssen wir ihn, um ihn richtig auszuwählen, nach unseren .mode Elementen platzieren. Dasselbe gilt für die <h1>, damit sie am Ende des Dokuments landet (sagen Sie das den SEO-Leuten nicht).

.mode {
  opacity: 0;
  pointer-events: none;

  &:target,
  &#start {
    opacity: 1;
    pointer-events: all;
    z-index: 1;
  }

  &#p1, &#p2 {
    position: absolute;
    transform: translateX(0);
    opacity: 1;
    z-index: 2;
  }

  &#p1:target {
    transform: translateX(50vw);

    +#p2 {
      transform: translateX(50vw);
      z-index: 2;
    }
  }

  &#p2 {
    transform: translateX(50vw);
    z-index: 1;
  }

&#pause:target {
    ~ #p1, ~ #p2 {
      opacity: 0;
    }
  }
}

#start {
  .mode:target ~ & {
    display: none;
  }
}

Der .mode div hat niemals Zeigerereignisse und ist immer vollständig transparent (sprich: inaktiv), außer für den Startbildschirm, der standardmäßig aktiviert ist und der gerade anvisierte Bildschirm. Ich setze ihn nicht einfach auf display: none;, weil er immer noch im Dokumentenfluss sein muss. Das Verstecken der Sichtbarkeit funktioniert nicht, weil ich später Zeigerereignisse einzeln aktivieren muss, wenn feindliche Schiffe getroffen werden.

Ich brauche #p1 und #p2 nebeneinander, weil das die Interaktion zwischen den Treffern eines Spielers und den Schiffen des anderen Spielers ermöglicht.

Implementierung der Schlachtfelder

Wir benötigen zwei Sätze von zwei Schlachtfeldern, also insgesamt vier Schlachtfelder. Jeder Satz enthält ein Schlachtfeld für den aktuellen Spieler und ein weiteres für den gegnerischen Spieler. Ein Satz wird sich in #p1 und der andere in #p2 befinden. Nur einer der Spieler wird im Viewport sein, aber beide behalten ihre Zeigerereignisse und ihren Fluss im Dokument. Hier eine kleine Skizze.

Jetzt brauchen wir viel HTML. Jeder Spieler benötigt zwei Schlachtfelder, die 12×12 Felder haben müssen. Das sind insgesamt 576 Felder, also werden wir ein wenig iterieren.

Die Felder erhalten ihre eigene Klasse, die ihre Position im Raster deklariert. Außerdem erhalten Felder in der ersten Reihe eine Positionsanzeige, damit Sie etwas Cooles sagen können wie „Feuer auf C6.“

each party in 'p1', 'p2']
  .mode(id=party)
    each faction in 'enemy', 'player']
      .battlefield(class=faction, class=party)
        each line in 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
          each col, colI in 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L']
            div(class='field-x' + (colI+1) + '-y' + line)
              if (col === 'A')
                .indicator-col #{line}
              if (line === 1)
                .indicator-line #{col}

Das Schlachtfeld selbst wird in einem CSS-Grid gesetzt, dessen Vorlage und Messungen von den zuvor definierten Variablen stammen. Wir positionieren sie absolut innerhalb unserer .mode divs und tauschen die Gegnerposition mit dem Spieler aus. Im eigentlichen Brettspiel haben Sie auch Ihre eigenen Schiffe unten. Beachten Sie, dass wir den calc für den oberen Wert maskieren müssen, sonst versucht Less, ihn für Sie zu berechnen und schlägt fehl.

.battlefield {
  position: absolute;
  display: grid;
  grid-template-columns: repeat(@gridSize, 1fr);
  width: var(--grid-measurements);
  height: var(--grid-measurements);
  margin: 0 auto 5vw;
  border: 2px solid;
  transform: translate(-50%, 0);
  z-index: @zSea;

  &.player {
    top: calc(var(--grid-measurements) ~"+" 150px);
    border-color: transparent;

    :target & {
      border-color: @playerColor;
    }
  }

  &.enemy {
    top: 100px;
    border-color: transparent;

    :target & {
      border-color: @enemyColor;
    }
  }
}

Wir möchten, dass die Kacheln des Schlachtfelds ein schönes Schachbrettmuster haben. Ich habe ein Mixin geschrieben, um die Farben zu berechnen, und da ich meine Mixins vom Rest getrennt mag, kommt dies in eine Datei namens components.less.

.checkerboard(@counter) when (@counter > 0) {
  .checkerboard(@counter - 2);

  &[class^='field-'][class$='-y@{counter}'] {
    &:nth-of-type(odd) {
      background-color: transparent;

      :target & {
      background-color: darken(@seaColor, 3%);
    }
  }

  &:nth-of-type(even) {
    background-color: transparent;

    :target & {
        background-color: darken(@seaColor, 4%);
      }
    }
  }
}

Wenn wir es mit .checkerboard(@gridSize); aufrufen, iteriert es durch jede zweite Zeile des Rasters und setzt Hintergrundfarben für ungerade und gerade Instanzen des aktuellen Elements. Die restlichen Felder können wir mit einem gewöhnlichen :odd und :even einfärben.

Als Nächstes platzieren wir die Indikatoren außerhalb der Schlachtfelder.

[class^='field-'] {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  background-color: transparent;
  
  .checkerboard(@gridSize);
  :target &:nth-of-type(even) {
    background-color: darken(@seaColor, 2%);
  }

  :target &:nth-of-type(odd) {
    background-color: darken(@seaColor, 1%);
  }

  [class^='indicator-'] {
    display: none;

    :target & {
      position: absolute;
      display: flex;
      justify-content: center;
      width: calc(var(--grid-measurements)~"/"@gridSize);
      height: calc(var(--grid-measurements)~"/"@gridSize);
      color: lighten(@seaColor, 10%);
      pointer-events: none;
    }

    &.indicator-line {
      top: -1.5em;
      align-items: flex-start;
    }

    &.indicator-col {
      left: -2.3em;
      align-items: center;
    }
  }
}

Implementierung der Schiffe

Kommen wir zum kniffligen Teil und platzieren wir einige Schiffe. Diese müssen klickbar und interaktiv sein, also werden es Checkboxen sein. Tatsächlich brauchen wir zwei Checkboxen für ein Schiff: Fehlschuss und Treffer.

  • Fehlschuss ist die untere. Wenn nichts anderes auf diesem Feld ist, trifft Ihr Schuss das Wasser und löst eine Fehlschuss-Animation aus. Die Ausnahme ist, wenn ein Spieler auf sein eigenes Schlachtfeld klickt. In diesem Fall spielt die Schiffsanimation.
  • Wenn ein eigenes Schiff erscheint, aktiviert es eine neue Checkbox. Diese wird als Treffer bezeichnet. Sie befindet sich an exakt denselben Koordinaten wie das entsprechende Schiff, jedoch im Angriffsfeld des anderen Spielers und oberhalb der Checkbox-Hilfe für den Fehlschuss. Wenn ein Treffer aktiviert wird, zeigt er eine Trefferanimation auf dem Angriffsfeld des aktuellen Spielers sowie auf dem eigenen Schiff des Gegners an.

Deshalb müssen wir unsere Schlachtfelder absolut nebeneinander positionieren. Wir müssen sie jederzeit aufeinander abstimmen, um sie miteinander interagieren zu lassen.

Zuerst legen wir einige Stile fest, die für beide Checkboxen gelten. Wir benötigen immer noch die Zeigerereignisse, möchten aber die Checkbox visuell ausblenden und stattdessen mit Hilfselementen arbeiten.

.check {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  
  + .check-helper {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
  }
}

Wir schreiben auch einige Klassen für unsere Ereignisse für die spätere Verwendung. Dies wird ebenfalls in components.less landen.

.hit-obj {
  position: absolute;
  visibility: visible;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  animation: hit 1s forwards;
}

.ship-obj {
  position: absolute;
  left: 0;
  top: 0;
  width: 90%;
  height: 90%;
  border-radius: 15%;
  animation: setShip 0.5s forwards;
}

.miss-obj {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  animation: miss 1s forwards;
}

Schiffe platzieren und Fehlschüsse

Diese beiden Ereignisse sind im Grunde gleich. Wenn Sie auf dem eigenen Schlachtfeld auf das Meer schießen, erstellen Sie ein Schiff. Wenn Sie auf dem feindlichen Schlachtfeld auf das Meer schießen, lösen Sie einen Fehlschuss aus. Dies geschieht durch Aufrufen der jeweiligen Klasse aus unserer components.less-Datei innerhalb eines Pseudo-Elements der Hilfsklasse. Wir verwenden hier Pseudo-Elemente, weil wir später zwei Objekte in einem Helfer platzieren müssen.

Wenn Sie ein Schiff platzieren, sollten Sie es nicht wieder entfernen können, daher lassen wir es nach dem Überprüfen seine Zeigerereignisse verlieren. Die nächste Treffer-Checkbox erhält jedoch ihre Zeigerereignisse, wodurch der Gegner platzierte Schiffe treffen kann.

.check {
  &.ship {
    &:checked {
      pointer-events: none;
    }

    &:checked + .check-helper {
      :target .player & {
        &::after {
          content: "";
          .ship-obj; // set own ship
        }
      }

      :target .enemy & {
        &::after {
          content: "";
          .miss-obj; // miss enemy ship
        }
      }
    }        

    &:checked ~ .hit {
      pointer-events: all;
    }
  }
}

Schiffe treffen

Diese neue Treffer-Checkbox ist absolut oben auf dem Angriffsfeld des anderen Spielers positioniert. Für Spieler 1 bedeutet das 50vw nach rechts und die Rasterhöhe + 50px Rand nach oben. Sie hat standardmäßig keine Zeigerereignisse, diese werden durch die in .ship:check ~ .hit gesetzten überschrieben, sodass nur tatsächlich platzierte Schiffe getroffen werden können.

Um ein Treffereignis anzuzeigen, benötigen wir zwei Pseudo-Elemente: eines, das den Treffer auf dem Angriffsfeld bestätigt; und eines, das dem Opfer zeigt, wo es getroffen wurde. :checked + .check-helper::after ruft ein .hit-obj aus components.less auf das Feld des Angreifers und das entsprechende ::before Pseudo-Element wird zurück auf das Schlachtfeld des Opfers verschoben.

Da die Anzeige von Treffereignissen nicht auf den aktiven Spieler beschränkt ist, müssen wir alle unnötigen Instanzen manuell mit display: none; entfernen.

.check {
  &.hit {
    position: absolute;
    top: ~"calc(-1 * (var(--grid-measurements) + 50px))";
    left: 50vw;
    width: 100%;
    height: 100%;
    pointer-events: none;

    #p2 &,
    #p1:target & {
      left: 0;
    }

    #p1:not(:target) & + .check-helper::before {
      left: 50vw;
    }

    &:checked {
      opacity: 1;
      visibility: hidden;
      pointer-events: none;

      + .check-helper {
        &::before {
          content: "";
          .hit-obj; // hit enemy ships
          top: ~"calc(-1 * (var(--grid-measurements) + 50px))";
      }

        &::after {
          content: "";
          .hit-obj; // hit own ships
          top: -2px;
          left: -2px;
        }

        #p1:target &::before,
        #p1:target ~ #p2 &::after,
        #p1:not(:target) &::after,
        #p2:target &::before {
          display: none;
        }
      }
    }

    #p1:target .battlefield.p1 &,
    #p2:target .battlefield.p2 & {
      display: none;
    }
  }
}

Animation der Ereignisse

Obwohl wir unsere Fehlschuss-, Schiff- und Treffer-Objekte gestylt haben, ist noch nichts zu sehen. Das liegt daran, dass uns noch die Animationen fehlen, die diese Objekte sichtbar machen. Dies sind einfache Keyframe-Animationen, die ich in eine neue Less-Datei namens animations.less geschrieben habe.

@keyframes setShip {
  0% {
    transform: scale(0, 0);
    background-color: transparent;
  }

  100% {
    transform: scale(1, 1);
    background-color: @playerColor;
  }
}

@keyframes hit {
  0% {
    transform: scale(0, 0);
    opacity: 0;
    background-color: transparent;
  }

  10% {
    transform: scale(1.2, 1.2);
    opacity: 1;
    background-color: spin(@hitColor, 40);
    box-shadow: 0 0 0 0.5em var(--shadowColor);
  }

  100% {
    transform: scale(.7, .7);
    opacity: .7;
    background-color: @hitColor;
    box-shadow: 0 0 0 0.5em var(--shadowColor);
  }
}

@keyframes miss {
  0% {
    transform: scale(0, 0);
    opacity: 1;
    background-color: lighten(@seaColor, 50);
  }

  100% {
    transform: scale(1, 1);
    opacity: .8;
    background-color: lighten(@seaColor, 10);
  }
}

Anpassbare Spielernamen hinzufügen

Das ist für die Funktionalität nicht wirklich notwendig, aber es ist ein schönes kleines Extra. Anstatt „Spieler 1“ und „Spieler 2“ genannt zu werden, können Sie Ihren eigenen Namen eingeben. Das machen wir, indem wir zwei <input type="text"> zu .status hinzufügen, einen für jeden Spieler. Sie haben Platzhalter, falls die Spieler ihre Namen nicht eingeben und sofort zum Spiel springen möchten.

.status
  input(type="text" placeholder="1st Player").player-name#name1
  input(type="text" placeholder="2nd Player").player-name#name2
  each party  in ['p1', 'p2']
      a.player-link.switch(href='#' + party)
  a.status-link.playpause(href='#pause') End Turn

Da wir sie in .status platziert haben, können wir sie auf jedem Bildschirm anzeigen. Auf dem Startbildschirm lassen wir sie als normale Eingabefelder, damit die Spieler ihre Namen eingeben können. Wir stylen ihre Platzhalter so, dass sie wie die tatsächliche Texteingabe aussehen, sodass es keine Rolle spielt, ob die Spieler ihre Namen eingeben oder nicht.

.status {
  .player-name {
    position: relative;
    padding: 3px;
    border: 1px solid @enemyColor;
    background: transparent;
    color: @playerColor;

    &::placeholder {
      color: @playerColor;
      opacity: 1; // Reset Firefox user agent styles
    }
  }
}

Auf den anderen Bildschirmen entfernen wir ihre typischen Eingabefeldstile sowie ihre Zeigerereignisse, sodass sie wie normaler, nicht veränderbarer Text erscheinen. .status enthält auch leere Links zur Auswahl der Spieler. Wir stylen diese Links so, dass sie tatsächliche Abmessungen haben und die Namenseingaben ohne Zeigerereignisse darüber anzeigen. Das Klicken auf einen Namen löst jetzt den Link aus und zielt auf den entsprechenden Modus.

.status {
  .mode#pause:target ~ & {
    top: 40vh;
    width: calc(100% ~"-" 40px);
    padding: 0 20px;
    text-align: center;
    z-index: @zAbove;

    .player-name,
    .player-link {
      position: absolute;
      display: block;
      width: 80%;
      max-width: 500px;
      height: 40px;
      margin: 0;
      padding: 0;

      &:nth-of-type(even) {
        top: 60px;
      }
    }

    .player-name {
      border: 0;
      text-align: center;
      pointer-events: none;
    }
  }
}

Die Spielerbildschirme müssen nur den aktiven Spieler anzeigen, also entfernen wir den anderen.

.status {
  .mode#p1:target ~ & #name2 {
    display: none;
  }
  
  .mode#p2:target ~ & #name1 {
    display: none;
  }
}

Einige Hinweise zu Internet Explorer und Edge: Microsoft-Browser haben das ::placeholder Pseudo-Element nicht implementiert. Während sie :-ms-input-placeholder für IE und ::-ms-input-placeholder unterstützen, sowie das Webkit-Präfix für Edge, funktionieren diese Präfixe nur, wenn ::placeholder nicht gesetzt ist. Soweit ich mit Platzhaltern gespielt habe, konnte ich sie nur entweder in den Microsoft-Browsern oder in allen anderen richtig stylen. Wenn jemand eine Lösung hat, bitte teilen Sie sie!

Alles zusammenfügen

Was wir bisher haben, ist ein funktionelles, aber nicht sehr schickes Spiel. Ich benutze den Startbildschirm, um einige Grundregeln zu klären. Da wir keine fest codierte Gewinnbedingung haben und nichts, was die Spieler daran hindert, ihre Schiffe wild überall zu platzieren, habe ich eine Notiz „Fairplay“ erstellt, die das gute alte Ehrenwort fördert.

.mode#start
  .battlefield.enemy
    ol
      li
        span You are this color.
      li
        span Your enemy is
        span this
        span color
      li
        span You may place your ships as follows:
        ul
          li 1 x 5 blocks
          li 2 x 4 blocks
          li 3 x 3 blocks
          li 4 x 2 blocks

Ich gehe nicht ins Detail, wie ich die Dinge genau nach meinen Wünschen gestaltet habe, da das meiste davon sehr einfaches CSS ist. Sie können das Endergebnis durchgehen, um sie herauszusuchen.

Wenn wir schließlich alle Teile zusammenfügen, erhalten wir das hier:

Siehe den Pen CSS Game: Battleships von Daniel Schulz (@iamschulz) auf CodePen.

Zusammenfassung

Werfen wir einen Blick zurück auf das, was wir erreicht haben.

HTML und CSS sind vielleicht keine Programmiersprachen, aber sie sind mächtige Werkzeuge in ihrem jeweiligen Bereich. Wir können Zustände mit Pseudoklassen verwalten und das DOM mit Pseudo-Elementen manipulieren.

Während die meisten von uns :hover und :focus ständig verwenden, bleibt :checked weitgehend unbemerkt, bestenfalls nur zum Stylen tatsächlicher Checkboxen und Radiobuttons. Checkboxen sind praktische kleine Werkzeuge, die uns helfen können, unnötiges JavaScript in unseren einfacheren Frontend-Funktionen loszuwerden. Ich würde nicht zögern, Dropdown- oder Off-Canvas-Menüs in reinen CSS-Projekten zu erstellen, solange die Anforderungen nicht zu kompliziert werden.

Ich wäre etwas vorsichtiger bei der Verwendung des :target-Selektors. Da er den URL-Hash-Wert verwendet, ist er nur für einen globalen Wert nutzbar. Ich denke, ich würde ihn zum Beispiel verwenden, um den aktuellen Absatz auf einer Inhaltsseite hervorzuheben, aber nicht für wiederverwendbare Elemente wie einen Slider oder ein Akkordeon-Menü. Er kann auch schnell unübersichtlich werden bei größeren Projekten, besonders wenn andere Teile davon den Hash-Wert steuern.

Das Erstellen des Spiels war eine Lernerfahrung für mich, bei der ich mit Pseudoselektoren interagierte und mit vielen Zeigerereignissen spielte. Wenn ich es noch einmal bauen müsste, würde ich sicherlich einen anderen Weg wählen, was ein gutes Ergebnis für mich ist. Ich sehe es definitiv nicht als produktionsreife oder auch nur saubere Lösung, und diese super spezifischen Selektoren sind ein Albtraum zu warten, aber es hat einige gute Teile, die ich in reale Projekte übertragen kann.

Am wichtigsten ist jedoch, dass es eine lustige Sache war.