diff --git a/.claude/CLAUDE.MD b/.claude/CLAUDE.MD deleted file mode 100644 index 5da2437..0000000 --- a/.claude/CLAUDE.MD +++ /dev/null @@ -1,120 +0,0 @@ -# Claude Code – Working Agreement - -## Workflow: Plan → Write Tests → Implement → Verify - -### 1. Plan First -Before writing any code, produce an explicit plan: -- Restate the requirement in your own words to confirm understanding. -- List every file you intend to create or modify. -- Identify risks or unknowns upfront. -- Wait for confirmation **only** when the plan reveals an ambiguity that cannot be resolved from context. Otherwise proceed immediately. - -### 2. Write Tests -Before implementing, write tests that should cover the new behaviour. -Only write tests for the new behaviour. - -### 3. Implement -Follow the plan. Do not add scope beyond what was agreed. - -### 4. Verify Every Requirement -After implementation, go through each requirement one-by-one and confirm it is satisfied: -- Run the relevant tests (unit, integration, or build check) for every changed module. -- If a requirement **cannot** be fulfilled, do **not** silently skip it — document it immediately (see *Unresolved Requirements* below). - ---- - -## No Code Without Verification (Testing) - -- Every new behaviour must be covered by at least one automated test before the task is considered done. -- Every bug fix must be accompanied by a regression test that fails before the fix and passes after. -- Run `./gradlew :modules::test` (or the appropriate Gradle task) and confirm a green build before marking work complete. -- If a test cannot be written for a legitimate reason, document it in `docs/unresolved.md` with an explanation. - ---- - -## Automatic Bug Fixing - -- When a test or build step fails, attempt to fix the root cause immediately — do **not** ask for permission. -- Apply the fix, re-run the verification, and continue until green. -- If the same failure persists after **three** fix attempts, stop, log the issue in `docs/unresolved.md`, and surface a concise summary. - ---- - -## Unresolved Requirements → `docs/unresolved.md` - -When a requirement or bug cannot be resolved, append an entry to `docs/unresolved.md`: - -```markdown -## [YYYY-MM-DD] - -**Requirement / Bug:** - - -**Root Cause (if known):** - - -**Attempted Fixes:** -1. -2. … - -**Suggested Next Step:** - -``` - -Create the file if it does not exist. Never delete existing entries. - ---- - -## Project Structure - -``` -. ← Repository root (multi-project Gradle setup) -├── build.gradle.kts ← Root build file (shared plugins, dependency versions) -├── settings.gradle.kts ← Gradle settings (declares all subprojects) -├── modules/ ← One subdirectory per microservice -│ └── / -│ ├── build.gradle.kts -│ └── src/ -└── docs/ ← Architecture Decision Records, API docs, unresolved issues - └── unresolved.md -``` - -### Conventions -- All microservices live under `modules/{service-name}`. Never place service code in the root. -- Shared configuration (dependency versions, plugin setup) belongs in the **root** `build.gradle.kts` or in `buildSrc` / a version catalog. -- `settings.gradle.kts` must include every module via `include(":modules:")`. -- Architecture decisions go in `docs/adr/` as numbered Markdown files (`ADR-001-.md`). -- API contracts live in `/docs/api/`. -- Unit tests extend `AnyFunSuite with Matchers` — no `@Test` annotations, no `: Unit` requirement -- Integration tests use `@QuarkusTest` with JUnit 5 — `@Test` methods must be explicitly typed `: Unit` -- Always exclude scala-library from Quarkus deps to avoid Scala 2 conflicts - -## Coverage Conventions -- Branch coverage must be at least 90% - unless there is a good reason not to. -- Line coverage must be at least 95% - unless there is a good reason not to. -- Method coverage must be at least 90% - unless there is a good reason not to. -- To check coverage use jacoco-reporter/scoverage_coverage_gaps.py modules/{service}/build/reports/scoverageTest/scoverage.xml -- IMPORTANT: modules/{service}/build/reports/scoverage/scoverage.xml is not used for coverage TEST calculation. Do not use it. - -## Agent Routing Rules - -### Use agents in PARALLEL when: -- Tasks touch different, independent microservices -- No shared files or state between tasks -- Example: "implement service-user AND service-orders simultaneously" - -### Use agents SEQUENTIALLY when: -- Tasks have dependencies (architect → implementer → test-writer) -- Shared API contracts are involved -- Example: design API first, then implement, then test - -## Quick-Reference Checklist - -Before considering any task done, confirm: - -- [ ] Plan was written and requirements restated -- [ ] All planned files were created / modified -- [ ] Automated tests cover the new behaviour -- [ ] `./gradlew build` (or scoped task) is green -- [ ] Each requirement has been explicitly verified -- [ ] Any unresolved items are logged in `docs/unresolved.md` \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index cecf2a5..2695d7e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,58 +1,54 @@ -# CLAUDE.md +# CLAUDE.md — NowChessSystems -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +## Stack +Scala 3.5.x · Quarkus + quarkus-scala3 · Hibernate/Jakarta · Lanterna TUI · K8s + ArgoCD + Kargo · Frontend TBD (Vite/React/Angular/Vue) -## Build & Test Commands +## Structure +``` +build.gradle.kts / settings.gradle.kts # root; include(":modules:<svc>") per service +modules/<svc>/build.gradle.kts + src/ +docs/adr/ docs/api/ docs/unresolved.md +``` +Versions in root `extra["VERSIONS"]`; modules read via `rootProject.extra["VERSIONS"] as Map<String,String>`. +## Commands ```bash -# Build everything ./gradlew build - -# Build a single module -./gradlew :modules:<service>:build - -# Run tests for a single module -./gradlew :modules:<service>:test - -# Run a specific test class -./gradlew :modules:<service>:test --tests "de.nowchess.<service>.<ClassName>" +./gradlew :modules:<svc>:build|test +./gradlew :modules:<svc>:test --tests "de.nowchess.<svc>.<Class>" ``` -The only current module is `core` (`modules/core`). +## Workflow +1. **Plan** — restate requirement, list files, flag risks. Proceed unless genuine ambiguity. +2. **Tests first** — cover only new behaviour. +3. **Implement** — no scope creep. +4. **Verify** — check each requirement; confirm green build. -## Architecture +## Scala/Quarkus Rules +- `given`/`using` only (no `implicit`); `Option`/`Either`/`Try` (no `null`/`.get`) +- `jakarta.*` only; reactive I/O (`Uni`/`Multi`), no blocking on event loop +- Always exclude `org.scala-lang:scala-library` from Quarkus BOM +- Unit tests: `extends AnyFunSuite with Matchers` — no `@Test`, no `: Unit` +- Integration tests: `@QuarkusTest` + JUnit 5 — `@Test` methods need explicit `: Unit` -**NowChessSystems** is a chess platform built as a Scala 3 + Quarkus microservice system. +## Coverage +Line ≥ 95% · Branch ≥ 90% · Method ≥ 90% (document exceptions) +Check: `jacoco-reporter/scoverage_coverage_gaps.py modules/{svc}/build/reports/scoverageTest/scoverage.xml` +⚠️ Use `scoverageTest/`, NOT `scoverage/`. -- Multi-module Gradle project; every service lives under `modules/{service-name}`. -- Shared dependency versions live in the root `build.gradle.kts` under `extra["VERSIONS"]`. -- Each module reads versions via `rootProject.extra["VERSIONS"] as Map<String, String>`. -- `settings.gradle.kts` must `include(":modules:<service>")` for every module. +## Bug Fixing +Fix failures immediately without asking. After 3 failed attempts → log in `docs/unresolved.md` + surface summary. -### Stack (ADR-001) -| Layer | Technology | -|---|---| -| Language | Scala 3.5.x | -| Backend framework | Quarkus + `quarkus-scala3` extension | -| Persistence | Hibernate / Jakarta Persistence | -| Frontend (TBD) | Vite; React/Angular/Vue under evaluation | -| TUI | Lanterna | -| Container orchestration | Kubernetes + ArgoCD + Kargo | +## Agents (new service) +Sequential: architect → scala-implementer → test-writer → gradle-builder → code-reviewer (review only, no self-fix) +Parallel: only when services are fully independent (no shared contracts/state). -### Key Scala 3 / Quarkus Rules -- Use `given`/`using`, not `implicit` (no Scala 2 idioms). -- Use `Option`/`Either`/`Try`, never `null` or `.get`. -- Jakarta annotations only (`jakarta.*`), never `javax.*`. -- Use reactive types (`Uni`, `Multi`) for I/O; no blocking calls on the event loop. -- **Always exclude `org.scala-lang:scala-library` from Quarkus BOM** to avoid Scala 2 conflicts. -- **Unit tests use `extends AnyFunSuite with Matchers`** — ScalaTest DSL, no `@Test` annotations needed. -- **Integration tests use `@QuarkusTest` with JUnit 5** — explicit `: Unit` return type still required on `@Test` methods. +## Unresolved (`docs/unresolved.md`) +Append only, never delete: +``` +## [YYYY-MM-DD] <title> +**Requirement/Bug:** **Root Cause:** **Attempted Fixes:** **Next Step:** +``` -### Agent Workflow (for new services) -1. **architect** → writes OpenAPI contract to `docs/api/{service}.yaml` and ADR to `docs/adr/`. -2. **scala-implementer** → reads contract, implements service under `modules/{service}/`. -3. **test-writer** → writes `@QuarkusTest` integration tests and `AnyFunSuite with Matchers` unit tests. -4. **gradle-builder** → resolves any build/dependency issues. -5. **code-reviewer** → reviews; reports findings back without self-fixing. - -Detailed working agreement (plan/verify/unresolved workflow) is in `.claude/CLAUDE.MD`. +## Done Checklist +- [ ] Plan written · files created/modified · tests green · requirements verified · unresolved logged diff --git a/jacoco-reporter/jacoco_coverage_gaps.py b/jacoco-reporter/jacoco_coverage_gaps.py deleted file mode 100644 index 950711f..0000000 --- a/jacoco-reporter/jacoco_coverage_gaps.py +++ /dev/null @@ -1,411 +0,0 @@ -#!/usr/bin/env python3 -""" -JaCoCo Coverage Gap Reporter -Parses a JaCoCo XML report and outputs missing line & branch (conditional) -coverage in a structured format that Claude Code agents can act on directly. - -Usage: - python jacoco_coverage_gaps.py <jacoco-report.xml> [--min-coverage 80] - python jacoco_coverage_gaps.py <jacoco-report.xml> --output json - python jacoco_coverage_gaps.py <jacoco-report.xml> --output markdown - python jacoco_coverage_gaps.py <jacoco-report.xml> --output agent (default) -""" - -import xml.etree.ElementTree as ET -import sys -import argparse -import json -from pathlib import Path -from dataclasses import dataclass, field -from typing import Optional - - -# --------------------------------------------------------------------------- -# Data classes -# --------------------------------------------------------------------------- - -@dataclass -class LineCoverage: - line_number: int - hits: int # 0 = not executed - branch_total: int = 0 # 0 = not a branch point - branch_covered: int = 0 - - @property - def is_uncovered(self) -> bool: - return self.hits == 0 - - @property - def is_partial_branch(self) -> bool: - return self.branch_total > 0 and self.branch_covered < self.branch_total - - -@dataclass -class MethodCoverage: - name: str - descriptor: str - first_line: Optional[int] - missed_instructions: int - covered_instructions: int - missed_branches: int - covered_branches: int - uncovered_lines: list[int] = field(default_factory=list) - partial_branch_lines: list[int] = field(default_factory=list) - - @property - def total_branches(self) -> int: - return self.missed_branches + self.covered_branches - - @property - def is_fully_covered(self) -> bool: - return self.missed_instructions == 0 and self.missed_branches == 0 - - @property - def branch_coverage_pct(self) -> float: - total = self.total_branches - return 100.0 * self.covered_branches / total if total else 100.0 - - @property - def line_coverage_pct(self) -> float: - total = self.missed_instructions + self.covered_instructions - return 100.0 * self.covered_instructions / total if total else 100.0 - - -@dataclass -class ClassCoverage: - class_name: str # e.g. com/example/Foo - source_file: Optional[str] - methods: list[MethodCoverage] = field(default_factory=list) - all_lines: list[LineCoverage] = field(default_factory=list) - - @property - def java_class_name(self) -> str: - return self.class_name.replace("/", ".") - - @property - def source_path(self) -> Optional[str]: - """Best-guess relative source path.""" - if self.source_file: - package = "/".join(self.class_name.split("/")[:-1]) - return f"src/main/java/{package}/{self.source_file}" if package else f"src/main/java/{self.source_file}" - return None - - @property - def uncovered_lines(self) -> list[int]: - return sorted({l.line_number for l in self.all_lines if l.is_uncovered}) - - @property - def partial_branch_lines(self) -> list[int]: - return sorted({l.line_number for l in self.all_lines if l.is_partial_branch}) - - @property - def missed_branches(self) -> int: - return sum(max(l.branch_total - l.branch_covered, 0) for l in self.all_lines) - - @property - def total_branches(self) -> int: - return sum(l.branch_total for l in self.all_lines) - - @property - def covered_branches(self) -> int: - return self.total_branches - self.missed_branches - - @property - def missed_lines(self) -> int: - return len(self.uncovered_lines) - - @property - def total_lines(self) -> int: - return len(self.all_lines) - - @property - def covered_lines(self) -> int: - return self.total_lines - self.missed_lines - - @property - def has_gaps(self) -> bool: - return bool(self.uncovered_lines or self.partial_branch_lines) - - -# --------------------------------------------------------------------------- -# Parser -# --------------------------------------------------------------------------- - -def parse_jacoco_xml(xml_path: str) -> list[ClassCoverage]: - """Parse a JaCoCo XML report into ClassCoverage objects.""" - tree = ET.parse(xml_path) - root = tree.getroot() - - results: list[ClassCoverage] = [] - - for package in root.iter("package"): - for cls_elem in package.findall("class"): - class_name = cls_elem.get("name", "") - source_file = cls_elem.get("sourcefilename") - - # Build method map from <method> children - methods: list[MethodCoverage] = [] - for m in cls_elem.findall("method"): - counters = {c.get("type"): c for c in m.findall("counter")} - - def _missed(t): return int(counters[t].get("missed", 0)) if t in counters else 0 - def _covered(t): return int(counters[t].get("covered", 0)) if t in counters else 0 - - methods.append(MethodCoverage( - name=m.get("name", ""), - descriptor=m.get("desc", ""), - first_line=int(m.get("line")) if m.get("line") else None, - missed_instructions=_missed("INSTRUCTION"), - covered_instructions=_covered("INSTRUCTION"), - missed_branches=_missed("BRANCH"), - covered_branches=_covered("BRANCH"), - )) - - cc = ClassCoverage( - class_name=class_name, - source_file=source_file, - methods=methods, - ) - - # Per-line data lives in the matching <sourcefile> element - source_file_elem = package.find(f"sourcefile[@name='{source_file}']") if source_file else None - if source_file_elem is not None: - for line_elem in source_file_elem.findall("line"): - nr = int(line_elem.get("nr", 0)) - mi = int(line_elem.get("mi", 0)) # missed instructions - ci = int(line_elem.get("ci", 0)) # covered instructions - mb = int(line_elem.get("mb", 0)) # missed branches - cb = int(line_elem.get("cb", 0)) # covered branches - hits = ci # ci > 0 means line was executed at least once - cc.all_lines.append(LineCoverage( - line_number=nr, - hits=hits, - branch_total=mb + cb, - branch_covered=cb, - )) - - if cc.has_gaps: - results.append(cc) - - return results - - -# --------------------------------------------------------------------------- -# Formatters -# --------------------------------------------------------------------------- - -def _compact_ranges(numbers: list[int]) -> str: - """Turn [1,2,3,5,7,8,9] -> '1-3, 5, 7-9'""" - if not numbers: - return "" - ranges = [] - start = prev = numbers[0] - for n in numbers[1:]: - if n == prev + 1: - prev = n - else: - ranges.append(f"{start}-{prev}" if start != prev else str(start)) - start = prev = n - ranges.append(f"{start}-{prev}" if start != prev else str(start)) - return ", ".join(ranges) - - -def format_agent(classes: list[ClassCoverage]) -> str: - """ - Output optimised for Claude Code agents: - – structured, machine-readable yet human-legible - – uses file paths and line numbers agents can act on - – groups by file, sorts by severity (most gaps first) - """ - lines: list[str] = [] - lines.append("# JaCoCo Coverage Gaps — Agent Action Report") - lines.append("") - lines.append("## Summary") - total_uncovered = sum(c.missed_lines for c in classes) - total_partial = sum(len(c.partial_branch_lines) for c in classes) - total_missed_branches = sum(c.missed_branches for c in classes) - lines.append(f"- Files with gaps : {len(classes)}") - lines.append(f"- Uncovered lines : {total_uncovered}") - lines.append(f"- Partial branches: {total_partial} lines affected") - lines.append(f"- Missed branches : {total_missed_branches} branch paths") - lines.append("") - lines.append("---") - lines.append("") - lines.append("## Files Requiring Tests") - lines.append("") - lines.append("> Each entry lists the SOURCE FILE PATH, the LINE NUMBERS that need") - lines.append("> coverage, and the METHODS that contain those gaps.") - lines.append("> Write or extend unit/integration tests to exercise these paths.") - lines.append("") - - # Sort: most uncovered lines first - sorted_classes = sorted(classes, key=lambda c: -(c.missed_lines + len(c.partial_branch_lines))) - - for cls in sorted_classes: - source = cls.source_path or f"(source unknown) {cls.java_class_name}" - lines.append(f"### `{source}`") - lines.append(f"**Class**: `{cls.java_class_name}`") - lines.append("") - - if cls.uncovered_lines: - lines.append(f"#### ❌ Uncovered Lines") - lines.append(f"Lines not executed at all: `{_compact_ranges(cls.uncovered_lines)}`") - lines.append("") - lines.append("**Methods with uncovered lines:**") - for method in cls.methods: - uncov = [l for l in cls.uncovered_lines - if method.first_line and l >= method.first_line] - # heuristic: only attribute if there are uncovered lines near the method start - if method.missed_instructions > 0: - sig = f"`{method.name}{method.descriptor}`" - pct = method.line_coverage_pct - lines.append(f" - {sig} — {pct:.0f}% instruction coverage") - lines.append("") - - if cls.partial_branch_lines: - lines.append(f"#### ⚠️ Partial Branch Coverage (Missing Conditional Paths)") - lines.append(f"Lines where not all branches are taken: `{_compact_ranges(cls.partial_branch_lines)}`") - lines.append("") - lines.append("**Methods with branch gaps:**") - for method in cls.methods: - if method.missed_branches > 0: - sig = f"`{method.name}{method.descriptor}`" - pct = method.branch_coverage_pct - missing = method.missed_branches - lines.append(f" - {sig} — {pct:.0f}% branch coverage ({missing} branch path(s) never taken)") - lines.append("") - - lines.append("**Action**: Add tests that exercise the above lines/branches.") - lines.append("") - lines.append("---") - lines.append("") - - lines.append("## Quick Reference: All Uncovered Locations") - lines.append("") - lines.append("Copy-paste friendly list for IDE navigation or grep:") - lines.append("") - lines.append("```") - for cls in sorted_classes: - src = cls.source_path or cls.java_class_name - if cls.uncovered_lines: - for ln in cls.uncovered_lines: - lines.append(f"{src}:{ln} # uncovered line") - if cls.partial_branch_lines: - for ln in cls.partial_branch_lines: - lines.append(f"{src}:{ln} # partial branch") - lines.append("```") - - return "\n".join(lines) - - -def format_json(classes: list[ClassCoverage]) -> str: - out = [] - for cls in classes: - out.append({ - "class": cls.java_class_name, - "source_path": cls.source_path, - "uncovered_lines": cls.uncovered_lines, - "partial_branch_lines": cls.partial_branch_lines, - "missed_branches": cls.missed_branches, - "methods": [ - { - "name": m.name, - "descriptor": m.descriptor, - "first_line": m.first_line, - "line_coverage_pct": round(m.line_coverage_pct, 1), - "branch_coverage_pct": round(m.branch_coverage_pct, 1), - "missed_branches": m.missed_branches, - "missed_instructions": m.missed_instructions, - } - for m in cls.methods - if not m.is_fully_covered - ], - }) - return json.dumps(out, indent=2) - - -def format_markdown(classes: list[ClassCoverage]) -> str: - lines: list[str] = [] - lines.append("# JaCoCo Missing Coverage Report\n") - for cls in sorted(classes, key=lambda c: cls.java_class_name): - lines.append(f"## {cls.java_class_name}") - if cls.source_path: - lines.append(f"**File**: `{cls.source_path}`\n") - if cls.uncovered_lines: - lines.append(f"**Uncovered lines**: {_compact_ranges(cls.uncovered_lines)}\n") - if cls.partial_branch_lines: - lines.append(f"**Partial branches at lines**: {_compact_ranges(cls.partial_branch_lines)}\n") - lines.append("| Method | Line Coverage | Branch Coverage | Missed Branches |") - lines.append("|--------|--------------|-----------------|-----------------|") - for m in cls.methods: - if not m.is_fully_covered: - lines.append( - f"| `{m.name}` | {m.line_coverage_pct:.0f}% | " - f"{m.branch_coverage_pct:.0f}% | {m.missed_branches} |" - ) - lines.append("") - return "\n".join(lines) - - -# --------------------------------------------------------------------------- -# Entry point -# --------------------------------------------------------------------------- - -def main() -> None: - parser = argparse.ArgumentParser( - description="Report missing line & branch coverage from a JaCoCo XML report." - ) - parser.add_argument("xml_file", help="Path to jacoco.xml report file") - parser.add_argument( - "--output", "-o", - choices=["agent", "json", "markdown"], - default="json", - help="Output format (default: agent)", - ) - parser.add_argument( - "--min-coverage", - type=float, - default=0.0, - help="Only report classes below this %% line coverage (0 = report all gaps)", - ) - parser.add_argument( - "--package-filter", "-p", - default=None, - help="Only report classes in this package prefix (e.g. com/example/service)", - ) - args = parser.parse_args() - - xml_path = Path(args.xml_file) - if not xml_path.exists(): - print(f"ERROR: File not found: {xml_path}", file=sys.stderr) - sys.exit(1) - - classes = parse_jacoco_xml(str(xml_path)) - - # Apply package filter - if args.package_filter: - prefix = args.package_filter.replace(".", "/") - classes = [c for c in classes if c.class_name.startswith(prefix)] - - # Apply min-coverage filter - if args.min_coverage > 0: - def _line_pct(c: ClassCoverage) -> float: - total = c.total_lines - return 100.0 * c.covered_lines / total if total else 100.0 - - classes = [c for c in classes if _line_pct(c) < args.min_coverage] - - if not classes: - print("✅ No coverage gaps found matching the given filters.") - return - - if args.output == "agent": - print(format_agent(classes)) - elif args.output == "json": - print(format_json(classes)) - elif args.output == "markdown": - print(format_markdown(classes)) - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/modules/core/src/main/scala/de/nowchess/chess/logic/MoveValidator.scala b/modules/core/src/main/scala/de/nowchess/chess/logic/MoveValidator.scala index 79c2e2d..5c485b7 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/logic/MoveValidator.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/logic/MoveValidator.scala @@ -132,29 +132,24 @@ object MoveValidator: val kingSq = Square(File.E, rank) val enemy = color.opposite - if ctx.board.pieceAt(kingSq) != Some(Piece(color, PieceType.King)) then return Set.empty - if GameRules.isInCheck(ctx.board, color) then return Set.empty + if !ctx.board.pieceAt(kingSq).contains(Piece(color, PieceType.King)) || + GameRules.isInCheck(ctx.board, color) then Set.empty + else + val kingsideSq = Option.when( + rights.kingSide && + ctx.board.pieceAt(Square(File.H, rank)).contains(Piece(color, PieceType.Rook)) && + List(Square(File.F, rank), Square(File.G, rank)).forall(s => ctx.board.pieceAt(s).isEmpty) && + !List(Square(File.F, rank), Square(File.G, rank)).exists(s => isAttackedBy(ctx.board, s, enemy)) + )(Square(File.G, rank)) - var result = Set.empty[Square] + val queensideSq = Option.when( + rights.queenSide && + ctx.board.pieceAt(Square(File.A, rank)).contains(Piece(color, PieceType.Rook)) && + List(Square(File.B, rank), Square(File.C, rank), Square(File.D, rank)).forall(s => ctx.board.pieceAt(s).isEmpty) && + !List(Square(File.D, rank), Square(File.C, rank)).exists(s => isAttackedBy(ctx.board, s, enemy)) + )(Square(File.C, rank)) - if rights.kingSide then - val rookSq = Square(File.H, rank) - val transit = List(Square(File.F, rank), Square(File.G, rank)) - if ctx.board.pieceAt(rookSq).contains(Piece(color, PieceType.Rook)) && - transit.forall(s => ctx.board.pieceAt(s).isEmpty) && - !transit.exists(s => isAttackedBy(ctx.board, s, enemy)) then - result += Square(File.G, rank) - - if rights.queenSide then - val rookSq = Square(File.A, rank) - val emptySquares = List(Square(File.B, rank), Square(File.C, rank), Square(File.D, rank)) - val transitSqs = List(Square(File.D, rank), Square(File.C, rank)) - if ctx.board.pieceAt(rookSq).contains(Piece(color, PieceType.Rook)) && - emptySquares.forall(s => ctx.board.pieceAt(s).isEmpty) && - !transitSqs.exists(s => isAttackedBy(ctx.board, s, enemy)) then - result += Square(File.C, rank) - - result + kingsideSq.toSet ++ queensideSq.toSet def legalTargets(ctx: GameContext, from: Square): Set[Square] = ctx.board.pieceAt(from) match diff --git a/modules/core/src/main/scala/de/nowchess/chess/view/Renderer.scala b/modules/core/src/main/scala/de/nowchess/chess/view/Renderer.scala index 3a7fafa..b572860 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/view/Renderer.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/view/Renderer.scala @@ -11,21 +11,18 @@ object Renderer: private val AnsiBlackPiece = "\u001b[30m" // black text def render(board: Board): String = - val sb = new StringBuilder - sb.append(" a b c d e f g h\n") - for rank <- (0 until 8).reverse do - sb.append(s"${rank + 1} ") - for file <- 0 until 8 do - val sq = Square(File.values(file), Rank.values(rank)) - val isLightSq = (file + rank) % 2 != 0 - val bgColor = if isLightSq then AnsiLightSquare else AnsiDarkSquare - val cellContent = board.pieceAt(sq) match + val rows = (0 until 8).reverse.map { rank => + val cells = (0 until 8).map { file => + val sq = Square(File.values(file), Rank.values(rank)) + val isLightSq = (file + rank) % 2 != 0 + val bgColor = if isLightSq then AnsiLightSquare else AnsiDarkSquare + board.pieceAt(sq) match case Some(piece) => val fgColor = if piece.color == Color.White then AnsiWhitePiece else AnsiBlackPiece s"$bgColor$fgColor ${piece.unicode} $AnsiReset" case None => s"$bgColor $AnsiReset" - sb.append(cellContent) - sb.append(s" ${rank + 1}\n") - sb.append(" a b c d e f g h\n") - sb.toString + }.mkString + s"${rank + 1} $cells ${rank + 1}" + }.mkString("\n") + s" a b c d e f g h\n$rows\n a b c d e f g h\n"