Initial commit
This commit is contained in:
commit
ae221e29bc
24 changed files with 1034 additions and 0 deletions
10
.claude/settings.local.json
Normal file
10
.claude/settings.local.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"WebSearch",
|
||||||
|
"Bash(lein run)",
|
||||||
|
"Bash(lein check:*)",
|
||||||
|
"WebFetch(domain:github.com)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
4
.direnv/CACHEDIR.TAG
Normal file
4
.direnv/CACHEDIR.TAG
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
Signature: 8a477f597d28d172789f06886806bc55
|
||||||
|
# This file is a cache directory tag created by direnv.
|
||||||
|
# For information about cache directory tags, see:
|
||||||
|
# http://www.brynosaurus.com/cachedir/
|
||||||
1
.direnv/flake-profile
Symbolic link
1
.direnv/flake-profile
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
flake-profile-3-link
|
||||||
1
.direnv/flake-profile-3-link
Symbolic link
1
.direnv/flake-profile-3-link
Symbolic link
|
|
@ -0,0 +1 @@
|
||||||
|
/nix/store/dm4m8lnm28n4iwzqcs4qh011kdla3dm0-env-env
|
||||||
2
.envrc
Normal file
2
.envrc
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
use flake
|
||||||
|
layout node
|
||||||
4
.lein-repl-history
Normal file
4
.lein-repl-history
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
(require '[lanterna.screen :as scr])
|
||||||
|
(keys (ns-publics 'lanterna.screen))
|
||||||
|
(require '[lanterna.terminal :as term])
|
||||||
|
(keys (ns-publics 'lanterna.terminal))
|
||||||
135
README.md
Normal file
135
README.md
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
# Knife Fight
|
||||||
|
|
||||||
|
A terminal-based card game where lower cards win and players bleed to death. Built with Clojure and Lanterna.
|
||||||
|
|
||||||
|
## Game Rules
|
||||||
|
|
||||||
|
### Objective
|
||||||
|
Force your opponent to bleed out (run out of deck cards).
|
||||||
|
|
||||||
|
### How to Play
|
||||||
|
|
||||||
|
1. **Setup**: Each player starts with a deck of cards (configurable, default is 2 suits = 27 cards) and draws 3 cards to their hand.
|
||||||
|
|
||||||
|
2. **Each Round**:
|
||||||
|
- Both players play one card simultaneously
|
||||||
|
- **Lower card wins** (Ace=1 is the lowest, highest numbered cards lose)
|
||||||
|
- Winner's card goes to loser's damage pile
|
||||||
|
- Both players draw back to 3 cards
|
||||||
|
- **Bleeding**: Each player discards cards from their deck equal to (damage pile sum ÷ 3, rounded up)
|
||||||
|
|
||||||
|
3. **Winning**: When a player runs out of deck cards (bleeds to death), they lose.
|
||||||
|
|
||||||
|
### Face Card Abilities
|
||||||
|
|
||||||
|
- **Jack (Parry)**: Nullifies the round completely - no damage dealt to either player
|
||||||
|
- **Queen (Field Dressing)**: Swap a card from your damage pile with opponent's played card (not yet implemented)
|
||||||
|
- **King (Feint)**: After cards are revealed, play a second card from your hand (not yet implemented)
|
||||||
|
- **Joker (Reflect)**: Reverse damage direction - opponent's card goes to their own damage pile
|
||||||
|
|
||||||
|
### Deck Configuration
|
||||||
|
|
||||||
|
You can configure the number of suits per player (1-4):
|
||||||
|
- **1 suit**: 14 cards per player (quick game)
|
||||||
|
- **2 suits**: 27 cards per player (standard game) [Default]
|
||||||
|
- **3 suits**: 40 cards per player (longer game)
|
||||||
|
- **4 suits**: 54 cards per player (maximum length game)
|
||||||
|
|
||||||
|
Each suit includes:
|
||||||
|
- 13 numbered cards (Ace through King)
|
||||||
|
- 1 Joker
|
||||||
|
|
||||||
|
## Installation & Running
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Leiningen (already in your Nix flake)
|
||||||
|
- Clojure (already in your Nix flake)
|
||||||
|
- A terminal with at least 80x24 size
|
||||||
|
|
||||||
|
### Running the Game
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/tepichord/Projects/knife-fight
|
||||||
|
lein run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building a Standalone JAR
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lein uberjar
|
||||||
|
java -jar target/uberjar/knife-fight-0.1.0-SNAPSHOT-standalone.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
## Game Controls
|
||||||
|
|
||||||
|
### Main Menu
|
||||||
|
- `1` - Start a new game
|
||||||
|
- `2` - Configure deck size
|
||||||
|
- `3` - View help/rules
|
||||||
|
- `4` - Quit
|
||||||
|
|
||||||
|
### During Gameplay
|
||||||
|
- `1`, `2`, `3` - Select card from your hand (numbered left to right)
|
||||||
|
- `Q` - Quit to main menu
|
||||||
|
|
||||||
|
### Configuration Screen
|
||||||
|
- `1`, `2`, `3`, `4` - Select number of suits per player
|
||||||
|
- `Q` - Return to main menu
|
||||||
|
|
||||||
|
## Game Architecture
|
||||||
|
|
||||||
|
The game follows functional programming principles with immutable data structures:
|
||||||
|
|
||||||
|
- **cards.clj**: Card data structures and operations
|
||||||
|
- **deck.clj**: Deck generation and manipulation (configurable sizes)
|
||||||
|
- **game.clj**: Game state management
|
||||||
|
- **config.clj**: Configuration system for deck sizes
|
||||||
|
- **rules.clj**: Game mechanics (damage, bleeding, abilities)
|
||||||
|
- **ai.clj**: AI opponent strategy
|
||||||
|
- **ui/**: Terminal UI layer
|
||||||
|
- **screen.clj**: Lanterna screen management
|
||||||
|
- **menu.clj**: Main menu and configuration screens
|
||||||
|
- **render.clj**: Game state rendering
|
||||||
|
- **input.clj**: Keyboard input handling
|
||||||
|
- **core.clj**: Main menu loop and game orchestration
|
||||||
|
|
||||||
|
## AI Strategy
|
||||||
|
|
||||||
|
The AI uses a scoring system to select cards:
|
||||||
|
- Prioritizes medium-value cards (4-7) for consistent wins
|
||||||
|
- Saves very low cards (Ace, 2, 3) for critical moments
|
||||||
|
- Uses Jokers strategically when opponent has high damage (>15)
|
||||||
|
- Adapts strategy based on hand size and opponent's damage
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
### REPL
|
||||||
|
```bash
|
||||||
|
lein repl
|
||||||
|
```
|
||||||
|
|
||||||
|
### Checking Code
|
||||||
|
```bash
|
||||||
|
lein check
|
||||||
|
```
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
|
||||||
|
- Queen (Field Dressing) ability requires UI selection - simplified in current version
|
||||||
|
- King (Feint) ability requires playing a second card - not yet implemented
|
||||||
|
- No save/load functionality
|
||||||
|
- No multiplayer support (only Human vs AI)
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
- Full implementation of Queen and King abilities
|
||||||
|
- Save/load game state
|
||||||
|
- Statistics tracking
|
||||||
|
- Multiple AI difficulty levels
|
||||||
|
- Tournament mode (best of N games)
|
||||||
|
- Network multiplayer
|
||||||
|
- Replay system
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0
|
||||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1767892417,
|
||||||
|
"narHash": "sha256-dhhvQY67aboBk8b0/u0XB6vwHdgbROZT3fJAjyNh5Ww=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "3497aa5c9457a9d88d71fa93a4a8368816fbeeba",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
37
flake.nix
Normal file
37
flake.nix
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
description = "A personal website configuration";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
config.allowUnfree = true;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
name = "env";
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
helix
|
||||||
|
zellij
|
||||||
|
jujutsu
|
||||||
|
_1password-cli
|
||||||
|
gh
|
||||||
|
direnv
|
||||||
|
watch
|
||||||
|
leiningen
|
||||||
|
clojure
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
11
project.clj
Normal file
11
project.clj
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
(defproject knife-fight "0.1.0-SNAPSHOT"
|
||||||
|
:description "A two-player terminal-based card game where lower cards win and players bleed to death"
|
||||||
|
:url "https://github.com/tepichord/knife-fight"
|
||||||
|
:license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
|
||||||
|
:url "https://www.eclipse.org/legal/epl-2.0/"}
|
||||||
|
:dependencies [[org.clojure/clojure "1.11.1"]
|
||||||
|
[clojure-lanterna "0.9.7"]]
|
||||||
|
:main ^:skip-aot knife-fight.core
|
||||||
|
:target-path "target/%s"
|
||||||
|
:profiles {:uberjar {:aot :all
|
||||||
|
:jvm-opts ["-Dclojure.compiler.direct-linking=true"]}})
|
||||||
49
src/knife_fight/ai.clj
Normal file
49
src/knife_fight/ai.clj
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
(ns knife-fight.ai
|
||||||
|
(:require [knife-fight.cards :as cards]
|
||||||
|
[knife-fight.game :as game]))
|
||||||
|
|
||||||
|
(defn score-card-play
|
||||||
|
"Scores a potential card play based on strategic value"
|
||||||
|
[card opponent-damage-sum hand-size]
|
||||||
|
(let [base-value (cards/card-value card)
|
||||||
|
card-type (cards/card-type card)]
|
||||||
|
(cond
|
||||||
|
;; Joker is powerful, save for high-damage situations
|
||||||
|
(= card-type :reflect)
|
||||||
|
(if (> opponent-damage-sum 15) 100 20)
|
||||||
|
|
||||||
|
;; Jack is defensive, good when we might lose
|
||||||
|
(= card-type :parry)
|
||||||
|
50
|
||||||
|
|
||||||
|
;; Queen allows damage manipulation
|
||||||
|
(= card-type :field-dressing)
|
||||||
|
60
|
||||||
|
|
||||||
|
;; King doubles damage effectiveness
|
||||||
|
(= card-type :feint)
|
||||||
|
70
|
||||||
|
|
||||||
|
;; For number cards, prefer lower values (to win rounds)
|
||||||
|
;; But save very low cards (1-3) for critical moments
|
||||||
|
:else
|
||||||
|
(cond
|
||||||
|
(<= base-value 3) (if (< hand-size 2) 80 30)
|
||||||
|
(<= base-value 7) 90 ; Medium cards are safe plays
|
||||||
|
:else 40)))) ; High cards are risky
|
||||||
|
|
||||||
|
(defn select-card
|
||||||
|
"AI selects the best card to play from hand"
|
||||||
|
[game-state]
|
||||||
|
(let [ai-hand (get-in game-state [:players :ai :hand])
|
||||||
|
human-damage (game/damage-pile-sum game-state :human)
|
||||||
|
hand-size (count ai-hand)]
|
||||||
|
|
||||||
|
;; Score each card and select the highest scoring one
|
||||||
|
(->> ai-hand
|
||||||
|
(map (fn [card]
|
||||||
|
{:card card
|
||||||
|
:score (score-card-play card human-damage hand-size)}))
|
||||||
|
(sort-by :score >)
|
||||||
|
first
|
||||||
|
:card)))
|
||||||
60
src/knife_fight/cards.clj
Normal file
60
src/knife_fight/cards.clj
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
(ns knife-fight.cards)
|
||||||
|
|
||||||
|
(def ranks
|
||||||
|
"Card ranks with their numeric values for comparison"
|
||||||
|
{:ace 1, :two 2, :three 3, :four 4, :five 5, :six 6, :seven 7,
|
||||||
|
:eight 8, :nine 9, :ten 10, :jack 11, :queen 12, :king 13, :joker 14})
|
||||||
|
|
||||||
|
(def face-cards #{:jack :queen :king :joker})
|
||||||
|
|
||||||
|
(defn create-card
|
||||||
|
"Creates a card map with suit and rank"
|
||||||
|
[suit rank]
|
||||||
|
{:suit suit
|
||||||
|
:rank rank
|
||||||
|
:value (get ranks rank)
|
||||||
|
:face? (contains? face-cards rank)})
|
||||||
|
|
||||||
|
(defn card-value
|
||||||
|
"Returns the numeric value of a card for comparison"
|
||||||
|
[card]
|
||||||
|
(:value card))
|
||||||
|
|
||||||
|
(defn face-card?
|
||||||
|
"Returns true if the card is a face card"
|
||||||
|
[card]
|
||||||
|
(:face? card))
|
||||||
|
|
||||||
|
(defn card-type
|
||||||
|
"Returns the type/ability of the card"
|
||||||
|
[card]
|
||||||
|
(cond
|
||||||
|
(= (:rank card) :jack) :parry
|
||||||
|
(= (:rank card) :queen) :field-dressing
|
||||||
|
(= (:rank card) :king) :feint
|
||||||
|
(= (:rank card) :joker) :reflect
|
||||||
|
:else :number))
|
||||||
|
|
||||||
|
(defn card-display-name
|
||||||
|
"Returns a human-readable display name for a card"
|
||||||
|
[card]
|
||||||
|
(let [rank-str (case (:rank card)
|
||||||
|
:ace "A"
|
||||||
|
:two "2"
|
||||||
|
:three "3"
|
||||||
|
:four "4"
|
||||||
|
:five "5"
|
||||||
|
:six "6"
|
||||||
|
:seven "7"
|
||||||
|
:eight "8"
|
||||||
|
:nine "9"
|
||||||
|
:ten "10"
|
||||||
|
:jack "J"
|
||||||
|
:queen "Q"
|
||||||
|
:king "K"
|
||||||
|
:joker "JKR")
|
||||||
|
suit-symbol (case (:suit card)
|
||||||
|
:red "♥"
|
||||||
|
:black "♠"
|
||||||
|
"?")]
|
||||||
|
(str rank-str suit-symbol)))
|
||||||
28
src/knife_fight/config.clj
Normal file
28
src/knife_fight/config.clj
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
(ns knife-fight.config)
|
||||||
|
|
||||||
|
(defonce config-state
|
||||||
|
(atom {:suits-per-player 2}))
|
||||||
|
|
||||||
|
(defn get-suits-per-player
|
||||||
|
"Returns the current suits-per-player setting"
|
||||||
|
[]
|
||||||
|
(:suits-per-player @config-state))
|
||||||
|
|
||||||
|
(defn set-suits-per-player!
|
||||||
|
"Sets the suits-per-player configuration (1-4 allowed)"
|
||||||
|
[n]
|
||||||
|
(when (and (integer? n) (<= 1 n 4))
|
||||||
|
(swap! config-state assoc :suits-per-player n)
|
||||||
|
true))
|
||||||
|
|
||||||
|
(defn cards-per-player
|
||||||
|
"Calculates total cards per player based on suits-per-player"
|
||||||
|
([]
|
||||||
|
(cards-per-player (get-suits-per-player)))
|
||||||
|
([suits]
|
||||||
|
(* suits 14))) ; 13 numbered cards + 1 joker per suit
|
||||||
|
|
||||||
|
(defn get-config
|
||||||
|
"Returns the current configuration map"
|
||||||
|
[]
|
||||||
|
@config-state)
|
||||||
118
src/knife_fight/core.clj
Normal file
118
src/knife_fight/core.clj
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
(ns knife-fight.core
|
||||||
|
(:require [knife-fight.game :as game]
|
||||||
|
[knife-fight.rules :as rules]
|
||||||
|
[knife-fight.ai :as ai]
|
||||||
|
[knife-fight.ui.screen :as screen]
|
||||||
|
[knife-fight.ui.render :as render]
|
||||||
|
[knife-fight.ui.input :as input]
|
||||||
|
[knife-fight.ui.menu :as menu])
|
||||||
|
(:gen-class))
|
||||||
|
|
||||||
|
(defn play-card
|
||||||
|
"Plays a card from player's hand"
|
||||||
|
[game-state player-key card-index]
|
||||||
|
(let [hand (get-in game-state [:players player-key :hand])
|
||||||
|
card (nth hand card-index nil)]
|
||||||
|
(when card
|
||||||
|
(-> game-state
|
||||||
|
(assoc-in [:current-round
|
||||||
|
(if (= player-key :human) :human-card :ai-card)]
|
||||||
|
card)
|
||||||
|
(update-in [:players player-key :hand]
|
||||||
|
(fn [h] (vec (concat (take card-index h)
|
||||||
|
(drop (inc card-index) h)))))))))
|
||||||
|
|
||||||
|
(defn process-turn
|
||||||
|
"Processes a single turn with both players' actions"
|
||||||
|
[game-state human-card-index]
|
||||||
|
(let [;; Human plays card
|
||||||
|
state-after-human (play-card game-state :human human-card-index)
|
||||||
|
|
||||||
|
;; AI selects and plays card
|
||||||
|
ai-card (ai/select-card state-after-human)
|
||||||
|
ai-hand (get-in state-after-human [:players :ai :hand])
|
||||||
|
ai-card-index (->> ai-hand
|
||||||
|
(map-indexed vector)
|
||||||
|
(filter #(= (second %) ai-card))
|
||||||
|
first
|
||||||
|
first)
|
||||||
|
state-after-ai (play-card state-after-human :ai ai-card-index)
|
||||||
|
|
||||||
|
;; Resolve the round
|
||||||
|
human-card (get-in state-after-ai [:current-round :human-card])
|
||||||
|
ai-card-played (get-in state-after-ai [:current-round :ai-card])
|
||||||
|
state-after-resolution (rules/resolve-round state-after-ai
|
||||||
|
human-card
|
||||||
|
ai-card-played)
|
||||||
|
|
||||||
|
;; End round (draw and bleed)
|
||||||
|
final-state (rules/end-round-phase state-after-resolution)]
|
||||||
|
|
||||||
|
final-state))
|
||||||
|
|
||||||
|
(defn game-loop
|
||||||
|
"Main game loop"
|
||||||
|
[]
|
||||||
|
(loop [game-state (game/create-initial-state)]
|
||||||
|
|
||||||
|
;; Render current state
|
||||||
|
(render/render-game-state game-state)
|
||||||
|
|
||||||
|
;; Check for game over
|
||||||
|
(if (game/game-over? game-state)
|
||||||
|
(do
|
||||||
|
(render/render-game-over
|
||||||
|
(assoc game-state :winner (game/determine-winner game-state)))
|
||||||
|
:game-over)
|
||||||
|
|
||||||
|
;; Get player input
|
||||||
|
(let [input-result (input/get-player-input)]
|
||||||
|
(case (:action input-result)
|
||||||
|
:quit :quit
|
||||||
|
|
||||||
|
:select
|
||||||
|
(let [card-index (:index input-result)
|
||||||
|
hand-size (game/hand-size game-state :human)]
|
||||||
|
(if (< card-index hand-size)
|
||||||
|
;; Valid card selection - process turn
|
||||||
|
(recur (process-turn game-state card-index))
|
||||||
|
;; Invalid selection - stay in same state
|
||||||
|
(recur game-state)))
|
||||||
|
|
||||||
|
;; No action or unknown action
|
||||||
|
(recur game-state))))))
|
||||||
|
|
||||||
|
(defn menu-loop
|
||||||
|
"Main menu loop"
|
||||||
|
[]
|
||||||
|
(loop []
|
||||||
|
(let [action (menu/handle-main-menu)]
|
||||||
|
(case action
|
||||||
|
:new-game
|
||||||
|
(do
|
||||||
|
(game-loop)
|
||||||
|
(recur))
|
||||||
|
|
||||||
|
:configure
|
||||||
|
(do
|
||||||
|
(menu/handle-config-menu)
|
||||||
|
(recur))
|
||||||
|
|
||||||
|
:help
|
||||||
|
(do
|
||||||
|
(menu/render-help)
|
||||||
|
(recur))
|
||||||
|
|
||||||
|
:quit
|
||||||
|
:quit
|
||||||
|
|
||||||
|
(recur)))))
|
||||||
|
|
||||||
|
(defn -main
|
||||||
|
"Entry point for the game"
|
||||||
|
[& args]
|
||||||
|
(try
|
||||||
|
(screen/init-screen!)
|
||||||
|
(menu-loop)
|
||||||
|
(finally
|
||||||
|
(screen/stop-screen!))))
|
||||||
43
src/knife_fight/deck.clj
Normal file
43
src/knife_fight/deck.clj
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
(ns knife-fight.deck
|
||||||
|
(:require [knife-fight.cards :as cards]
|
||||||
|
[knife-fight.config :as config]))
|
||||||
|
|
||||||
|
(def all-ranks [:ace :two :three :four :five :six :seven :eight :nine :ten :jack :queen :king])
|
||||||
|
|
||||||
|
(defn create-suit
|
||||||
|
"Creates a single suit of 13 cards + 1 joker for the given color"
|
||||||
|
[color]
|
||||||
|
(let [numbered-cards (mapv #(cards/create-card color %) all-ranks)
|
||||||
|
joker (cards/create-card color :joker)]
|
||||||
|
(conj numbered-cards joker)))
|
||||||
|
|
||||||
|
(defn create-decks
|
||||||
|
"Creates red and black decks based on the number of suits per player"
|
||||||
|
[suits-per-player]
|
||||||
|
(let [red-cards (vec (apply concat (repeatedly suits-per-player #(create-suit :red))))
|
||||||
|
black-cards (vec (apply concat (repeatedly suits-per-player #(create-suit :black))))]
|
||||||
|
{:red (shuffle red-cards)
|
||||||
|
:black (shuffle black-cards)}))
|
||||||
|
|
||||||
|
(defn create-default-decks
|
||||||
|
"Creates decks using the current configuration"
|
||||||
|
[]
|
||||||
|
(create-decks (config/get-suits-per-player)))
|
||||||
|
|
||||||
|
(defn draw-card
|
||||||
|
"Draws a card from deck, returns [card remaining-deck]"
|
||||||
|
[deck]
|
||||||
|
(when (seq deck)
|
||||||
|
[(first deck) (vec (rest deck))]))
|
||||||
|
|
||||||
|
(defn draw-cards
|
||||||
|
"Draws n cards from deck, returns [cards remaining-deck]"
|
||||||
|
[deck n]
|
||||||
|
(let [drawn (take n deck)
|
||||||
|
remaining (vec (drop n deck))]
|
||||||
|
[(vec drawn) remaining]))
|
||||||
|
|
||||||
|
(defn deck-size
|
||||||
|
"Returns the number of cards in a deck"
|
||||||
|
[deck]
|
||||||
|
(count deck))
|
||||||
64
src/knife_fight/game.clj
Normal file
64
src/knife_fight/game.clj
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
(ns knife-fight.game
|
||||||
|
(:require [knife-fight.deck :as deck]
|
||||||
|
[knife-fight.cards :as cards]))
|
||||||
|
|
||||||
|
(defn create-initial-state
|
||||||
|
"Creates a new game with shuffled and split decks"
|
||||||
|
[]
|
||||||
|
(let [{:keys [red black]} (deck/create-default-decks)
|
||||||
|
[human-hand human-deck] (deck/draw-cards red 3)
|
||||||
|
[ai-hand ai-deck] (deck/draw-cards black 3)]
|
||||||
|
{:players {:human {:hand human-hand
|
||||||
|
:deck human-deck
|
||||||
|
:damage-pile []
|
||||||
|
:name "Player"}
|
||||||
|
:ai {:hand ai-hand
|
||||||
|
:deck ai-deck
|
||||||
|
:damage-pile []
|
||||||
|
:name "Computer"}}
|
||||||
|
:current-round {:human-card nil
|
||||||
|
:ai-card nil
|
||||||
|
:winner nil}
|
||||||
|
:round-number 1
|
||||||
|
:game-over? false
|
||||||
|
:winner nil}))
|
||||||
|
|
||||||
|
(defn get-player
|
||||||
|
"Gets a player's state from game state"
|
||||||
|
[game-state player-key]
|
||||||
|
(get-in game-state [:players player-key]))
|
||||||
|
|
||||||
|
(defn update-player
|
||||||
|
"Updates a player's state in the game"
|
||||||
|
[game-state player-key update-fn]
|
||||||
|
(update-in game-state [:players player-key] update-fn))
|
||||||
|
|
||||||
|
(defn hand-size
|
||||||
|
"Returns the number of cards in a player's hand"
|
||||||
|
[game-state player-key]
|
||||||
|
(count (get-in game-state [:players player-key :hand])))
|
||||||
|
|
||||||
|
(defn deck-size
|
||||||
|
"Returns the number of cards in a player's deck"
|
||||||
|
[game-state player-key]
|
||||||
|
(count (get-in game-state [:players player-key :deck])))
|
||||||
|
|
||||||
|
(defn damage-pile-sum
|
||||||
|
"Calculates total damage for a player"
|
||||||
|
[game-state player-key]
|
||||||
|
(reduce + 0 (map cards/card-value
|
||||||
|
(get-in game-state [:players player-key :damage-pile]))))
|
||||||
|
|
||||||
|
(defn game-over?
|
||||||
|
"Checks if game is over (a player has no deck cards left)"
|
||||||
|
[game-state]
|
||||||
|
(or (zero? (deck-size game-state :human))
|
||||||
|
(zero? (deck-size game-state :ai))))
|
||||||
|
|
||||||
|
(defn determine-winner
|
||||||
|
"Determines the winner when game is over"
|
||||||
|
[game-state]
|
||||||
|
(cond
|
||||||
|
(zero? (deck-size game-state :human)) :ai
|
||||||
|
(zero? (deck-size game-state :ai)) :human
|
||||||
|
:else nil))
|
||||||
95
src/knife_fight/rules.clj
Normal file
95
src/knife_fight/rules.clj
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
(ns knife-fight.rules
|
||||||
|
(:require [knife-fight.game :as game]
|
||||||
|
[knife-fight.cards :as cards]
|
||||||
|
[knife-fight.deck :as deck]))
|
||||||
|
|
||||||
|
(defn compare-cards
|
||||||
|
"Compares two cards, returns :human/:ai/:tie. Lower value wins."
|
||||||
|
[human-card ai-card]
|
||||||
|
(let [human-val (cards/card-value human-card)
|
||||||
|
ai-val (cards/card-value ai-card)]
|
||||||
|
(cond
|
||||||
|
(< human-val ai-val) :human
|
||||||
|
(> human-val ai-val) :ai
|
||||||
|
:else :tie)))
|
||||||
|
|
||||||
|
(defn resolve-round
|
||||||
|
"Resolves a round: compare cards, apply abilities, deal damage"
|
||||||
|
[game-state human-card ai-card]
|
||||||
|
(let [human-type (cards/card-type human-card)
|
||||||
|
ai-type (cards/card-type ai-card)
|
||||||
|
winner (compare-cards human-card ai-card)]
|
||||||
|
|
||||||
|
(cond
|
||||||
|
;; Both Jacks = double parry, no damage
|
||||||
|
(and (= human-type :parry) (= ai-type :parry))
|
||||||
|
(assoc-in game-state [:current-round :winner] :tie)
|
||||||
|
|
||||||
|
;; One Jack = parry, no damage
|
||||||
|
(or (= human-type :parry) (= ai-type :parry))
|
||||||
|
(assoc-in game-state [:current-round :winner] :tie)
|
||||||
|
|
||||||
|
;; Joker reflects damage back
|
||||||
|
(= human-type :reflect)
|
||||||
|
(-> game-state
|
||||||
|
(game/update-player :human #(update % :damage-pile conj ai-card))
|
||||||
|
(assoc-in [:current-round :winner] :human))
|
||||||
|
|
||||||
|
(= ai-type :reflect)
|
||||||
|
(-> game-state
|
||||||
|
(game/update-player :ai #(update % :damage-pile conj human-card))
|
||||||
|
(assoc-in [:current-round :winner] :ai))
|
||||||
|
|
||||||
|
;; Tie: both take damage
|
||||||
|
(= winner :tie)
|
||||||
|
(-> game-state
|
||||||
|
(game/update-player :human #(update % :damage-pile conj ai-card))
|
||||||
|
(game/update-player :ai #(update % :damage-pile conj human-card))
|
||||||
|
(assoc-in [:current-round :winner] :tie))
|
||||||
|
|
||||||
|
;; Normal damage: winner's card goes to loser's damage pile
|
||||||
|
:else
|
||||||
|
(let [loser (if (= winner :human) :ai :human)
|
||||||
|
damage-card (if (= winner :human) human-card ai-card)]
|
||||||
|
(-> game-state
|
||||||
|
(game/update-player loser #(update % :damage-pile conj damage-card))
|
||||||
|
(assoc-in [:current-round :winner] winner))))))
|
||||||
|
|
||||||
|
(defn draw-to-hand
|
||||||
|
"Draws cards until player has 3 cards in hand"
|
||||||
|
[game-state player-key]
|
||||||
|
(let [current-hand-size (game/hand-size game-state player-key)
|
||||||
|
cards-needed (- 3 current-hand-size)
|
||||||
|
player-deck (get-in game-state [:players player-key :deck])]
|
||||||
|
(if (and (pos? cards-needed) (>= (count player-deck) cards-needed))
|
||||||
|
(let [[drawn remaining] (deck/draw-cards player-deck cards-needed)]
|
||||||
|
(-> game-state
|
||||||
|
(update-in [:players player-key :hand] concat drawn)
|
||||||
|
(assoc-in [:players player-key :deck] remaining)))
|
||||||
|
game-state)))
|
||||||
|
|
||||||
|
(defn calculate-bleed
|
||||||
|
"Calculates bleed amount: damage pile sum ÷ 3, rounded up"
|
||||||
|
[damage-pile]
|
||||||
|
(let [total-damage (reduce + 0 (map cards/card-value damage-pile))]
|
||||||
|
(int (Math/ceil (/ total-damage 3.0)))))
|
||||||
|
|
||||||
|
(defn apply-bleed
|
||||||
|
"Applies bleed damage: discard cards from deck"
|
||||||
|
[game-state player-key]
|
||||||
|
(let [bleed-amount (calculate-bleed
|
||||||
|
(get-in game-state [:players player-key :damage-pile]))
|
||||||
|
current-deck (get-in game-state [:players player-key :deck])
|
||||||
|
cards-to-bleed (min bleed-amount (count current-deck))
|
||||||
|
remaining-deck (vec (drop cards-to-bleed current-deck))]
|
||||||
|
(assoc-in game-state [:players player-key :deck] remaining-deck)))
|
||||||
|
|
||||||
|
(defn end-round-phase
|
||||||
|
"Completes end of round: draw to 3, then bleed both players"
|
||||||
|
[game-state]
|
||||||
|
(-> game-state
|
||||||
|
(draw-to-hand :human)
|
||||||
|
(draw-to-hand :ai)
|
||||||
|
(apply-bleed :human)
|
||||||
|
(apply-bleed :ai)
|
||||||
|
(update :round-number inc)))
|
||||||
20
src/knife_fight/ui/input.clj
Normal file
20
src/knife_fight/ui/input.clj
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
(ns knife-fight.ui.input
|
||||||
|
(:require [knife-fight.ui.screen :as screen]))
|
||||||
|
|
||||||
|
(defn parse-input
|
||||||
|
"Parses keyboard input into game actions"
|
||||||
|
[key-input]
|
||||||
|
(case key-input
|
||||||
|
\1 {:action :select :index 0}
|
||||||
|
\2 {:action :select :index 1}
|
||||||
|
\3 {:action :select :index 2}
|
||||||
|
\4 {:action :select :index 3}
|
||||||
|
\q {:action :quit}
|
||||||
|
\Q {:action :quit}
|
||||||
|
nil))
|
||||||
|
|
||||||
|
(defn get-player-input
|
||||||
|
"Gets and parses player input"
|
||||||
|
[]
|
||||||
|
(let [key (screen/get-key)]
|
||||||
|
(parse-input key)))
|
||||||
120
src/knife_fight/ui/menu.clj
Normal file
120
src/knife_fight/ui/menu.clj
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
(ns knife-fight.ui.menu
|
||||||
|
(:require [knife-fight.ui.screen :as screen]
|
||||||
|
[knife-fight.ui.input :as input]
|
||||||
|
[knife-fight.config :as config]))
|
||||||
|
|
||||||
|
(def title-color {:fg :red :styles #{:bold}})
|
||||||
|
(def option-color {:fg :white})
|
||||||
|
(def highlight-color {:fg :yellow :styles #{:bold}})
|
||||||
|
|
||||||
|
(defn render-main-menu
|
||||||
|
"Renders the main menu"
|
||||||
|
[]
|
||||||
|
(screen/clear-screen!)
|
||||||
|
(let [[width height] (screen/get-size)
|
||||||
|
center-x (/ width 2)
|
||||||
|
start-y (/ height 3)]
|
||||||
|
|
||||||
|
;; Title
|
||||||
|
(screen/put-string! (- center-x 10) start-y "=== KNIFE FIGHT ===" title-color)
|
||||||
|
|
||||||
|
;; Menu options
|
||||||
|
(screen/put-string! (- center-x 5) (+ start-y 2) "1. New Game" option-color)
|
||||||
|
(screen/put-string! (- center-x 10) (+ start-y 3) "2. Configure Deck Size" option-color)
|
||||||
|
(screen/put-string! (- center-x 6) (+ start-y 4) "3. Help / Rules" option-color)
|
||||||
|
(screen/put-string! (- center-x 3) (+ start-y 5) "4. Quit" option-color)
|
||||||
|
|
||||||
|
(screen/put-string! (- center-x 15) (+ start-y 7) "Select an option (1-4)" highlight-color))
|
||||||
|
|
||||||
|
(screen/hide-cursor!)
|
||||||
|
(screen/redraw!))
|
||||||
|
|
||||||
|
(defn render-config-menu
|
||||||
|
"Renders the deck configuration menu"
|
||||||
|
[]
|
||||||
|
(screen/clear-screen!)
|
||||||
|
(let [[width height] (screen/get-size)
|
||||||
|
center-x (/ width 2)
|
||||||
|
start-y (/ height 4)
|
||||||
|
current-suits (config/get-suits-per-player)]
|
||||||
|
|
||||||
|
;; Title
|
||||||
|
(screen/put-string! (- center-x 12) start-y "=== CONFIGURE DECK SIZE ===" title-color)
|
||||||
|
|
||||||
|
(screen/put-string! (- center-x 20) (+ start-y 2)
|
||||||
|
(str "Current setting: " current-suits " suits per player")
|
||||||
|
highlight-color)
|
||||||
|
|
||||||
|
;; Options
|
||||||
|
(screen/put-string! (- center-x 15) (+ start-y 4)
|
||||||
|
"1. 1 Suit (14 cards per player)"
|
||||||
|
(if (= current-suits 1) highlight-color option-color))
|
||||||
|
(screen/put-string! (- center-x 15) (+ start-y 5)
|
||||||
|
"2. 2 Suits (27 cards per player) [Default]"
|
||||||
|
(if (= current-suits 2) highlight-color option-color))
|
||||||
|
(screen/put-string! (- center-x 15) (+ start-y 6)
|
||||||
|
"3. 3 Suits (40 cards per player)"
|
||||||
|
(if (= current-suits 3) highlight-color option-color))
|
||||||
|
(screen/put-string! (- center-x 15) (+ start-y 7)
|
||||||
|
"4. 4 Suits (54 cards per player)"
|
||||||
|
(if (= current-suits 4) highlight-color option-color))
|
||||||
|
|
||||||
|
(screen/put-string! (- center-x 20) (+ start-y 9)
|
||||||
|
"Select deck size (1-4) or Q to return"
|
||||||
|
option-color))
|
||||||
|
|
||||||
|
(screen/hide-cursor!)
|
||||||
|
(screen/redraw!))
|
||||||
|
|
||||||
|
(defn render-help
|
||||||
|
"Renders the help/rules screen"
|
||||||
|
[]
|
||||||
|
(screen/clear-screen!)
|
||||||
|
|
||||||
|
(screen/put-string! 2 1 "=== KNIFE FIGHT RULES ===" title-color)
|
||||||
|
|
||||||
|
(screen/put-string! 2 3 "GOAL: Force your opponent to bleed out (run out of deck cards)")
|
||||||
|
(screen/put-string! 2 5 "HOW TO PLAY:")
|
||||||
|
(screen/put-string! 2 6 "1. Each round, both players play one card")
|
||||||
|
(screen/put-string! 2 7 "2. LOWER card wins the round (Ace=1 is lowest)")
|
||||||
|
(screen/put-string! 2 8 "3. Winner's card goes to loser's damage pile")
|
||||||
|
(screen/put-string! 2 9 "4. Draw back to 3 cards")
|
||||||
|
(screen/put-string! 2 10 "5. BLEED: Discard cards from deck = (damage sum / 3, rounded up)")
|
||||||
|
|
||||||
|
(screen/put-string! 2 12 "FACE CARDS:" highlight-color)
|
||||||
|
(screen/put-string! 2 13 "Jack (Parry): Nullifies round, no damage" option-color)
|
||||||
|
(screen/put-string! 2 14 "Queen (Field Dress): Swap damage pile card" option-color)
|
||||||
|
(screen/put-string! 2 15 "King (Feint): Play second card from hand" option-color)
|
||||||
|
(screen/put-string! 2 16 "Joker (Reflect): Reverse damage direction" option-color)
|
||||||
|
|
||||||
|
(screen/put-string! 2 18 "Press any key to return to menu..." highlight-color)
|
||||||
|
|
||||||
|
(screen/hide-cursor!)
|
||||||
|
(screen/redraw!)
|
||||||
|
(screen/get-key))
|
||||||
|
|
||||||
|
(defn handle-main-menu
|
||||||
|
"Handles main menu input and returns action"
|
||||||
|
[]
|
||||||
|
(render-main-menu)
|
||||||
|
(let [input (input/get-player-input)]
|
||||||
|
(case (:index input)
|
||||||
|
0 :new-game
|
||||||
|
1 :configure
|
||||||
|
2 :help
|
||||||
|
3 :quit
|
||||||
|
(if (= (:action input) :quit)
|
||||||
|
:quit
|
||||||
|
(recur)))))
|
||||||
|
|
||||||
|
(defn handle-config-menu
|
||||||
|
"Handles configuration menu input"
|
||||||
|
[]
|
||||||
|
(render-config-menu)
|
||||||
|
(let [input (input/get-player-input)]
|
||||||
|
(if (= (:action input) :quit)
|
||||||
|
:menu
|
||||||
|
(do
|
||||||
|
(when-let [idx (:index input)]
|
||||||
|
(config/set-suits-per-player! (inc idx)))
|
||||||
|
(recur)))))
|
||||||
108
src/knife_fight/ui/render.clj
Normal file
108
src/knife_fight/ui/render.clj
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
(ns knife-fight.ui.render
|
||||||
|
(:require [knife-fight.ui.screen :as screen]
|
||||||
|
[knife-fight.game :as game]
|
||||||
|
[knife-fight.cards :as cards]
|
||||||
|
[knife-fight.rules :as rules]))
|
||||||
|
|
||||||
|
;; Color schemes
|
||||||
|
(def colors
|
||||||
|
{:red {:fg :red}
|
||||||
|
:black {:fg :white}
|
||||||
|
:highlight {:fg :yellow :styles #{:bold}}
|
||||||
|
:damage {:fg :red :styles #{:bold}}
|
||||||
|
:info {:fg :cyan}
|
||||||
|
:title {:fg :white :styles #{:bold}}})
|
||||||
|
|
||||||
|
(defn render-hand
|
||||||
|
"Renders a player's hand at specified position"
|
||||||
|
[x y hand player-name]
|
||||||
|
(screen/put-string! x y (str player-name "'s Hand:") (:title colors))
|
||||||
|
(doseq [[idx card] (map-indexed vector hand)]
|
||||||
|
(let [card-str (str (inc idx) ". " (cards/card-display-name card))
|
||||||
|
style (if (= :red (:suit card)) (:red colors) (:black colors))]
|
||||||
|
(screen/put-string! x (+ y 1 idx) card-str style))))
|
||||||
|
|
||||||
|
(defn render-deck-info
|
||||||
|
"Renders deck and damage pile information"
|
||||||
|
[x y player-key game-state]
|
||||||
|
(let [deck-count (game/deck-size game-state player-key)
|
||||||
|
damage-sum (game/damage-pile-sum game-state player-key)
|
||||||
|
damage-cards (get-in game-state [:players player-key :damage-pile])
|
||||||
|
bleed (rules/calculate-bleed damage-cards)]
|
||||||
|
(screen/put-string! x y (str "Deck: " deck-count " cards") (:info colors))
|
||||||
|
(screen/put-string! x (inc y)
|
||||||
|
(str "Damage: " damage-sum " (Bleed: " bleed ")")
|
||||||
|
(:damage colors))
|
||||||
|
(screen/put-string! x (+ y 2) "Damage Pile:")
|
||||||
|
(doseq [[idx card] (map-indexed vector (take 5 damage-cards))]
|
||||||
|
(screen/put-string! x (+ y 3 idx) (str " " (cards/card-display-name card))))))
|
||||||
|
|
||||||
|
(defn render-round-result
|
||||||
|
"Renders the result of the current round"
|
||||||
|
[x y game-state]
|
||||||
|
(let [human-card (get-in game-state [:current-round :human-card])
|
||||||
|
ai-card (get-in game-state [:current-round :ai-card])
|
||||||
|
winner (get-in game-state [:current-round :winner])]
|
||||||
|
(when (and human-card ai-card)
|
||||||
|
(screen/put-string! x y "Last Round:" (:title colors))
|
||||||
|
(screen/put-string! x (inc y)
|
||||||
|
(str "You played: " (cards/card-display-name human-card)))
|
||||||
|
(screen/put-string! x (+ y 2)
|
||||||
|
(str "AI played: " (cards/card-display-name ai-card)))
|
||||||
|
(screen/put-string! x (+ y 3)
|
||||||
|
(str "Winner: " (case winner
|
||||||
|
:human "You!"
|
||||||
|
:ai "Computer"
|
||||||
|
:tie "Tie (no damage)"))
|
||||||
|
(:highlight colors)))))
|
||||||
|
|
||||||
|
(defn render-game-state
|
||||||
|
"Main rendering function - renders entire game state"
|
||||||
|
[game-state]
|
||||||
|
(screen/clear-screen!)
|
||||||
|
|
||||||
|
(let [human-hand (get-in game-state [:players :human :hand])
|
||||||
|
ai-hand-count (count (get-in game-state [:players :ai :hand]))]
|
||||||
|
|
||||||
|
;; Title
|
||||||
|
(screen/put-string! 2 1 "=== KNIFE FIGHT ===" (:title colors))
|
||||||
|
(screen/put-string! 2 2
|
||||||
|
(str "Round " (:round-number game-state))
|
||||||
|
(:info colors))
|
||||||
|
|
||||||
|
;; AI player area (top)
|
||||||
|
(screen/put-string! 2 4 "Computer's Hand:" (:title colors))
|
||||||
|
(screen/put-string! 2 5 (str ai-hand-count " cards"))
|
||||||
|
(render-deck-info 40 4 :ai game-state)
|
||||||
|
|
||||||
|
;; Round result if available
|
||||||
|
(render-round-result 2 11 game-state)
|
||||||
|
|
||||||
|
;; Human player area (bottom)
|
||||||
|
(render-hand 2 18 human-hand "Player")
|
||||||
|
(render-deck-info 40 18 :human game-state)
|
||||||
|
|
||||||
|
;; Instructions
|
||||||
|
(screen/put-string! 2 22
|
||||||
|
"Use 1-3 to select card, Q to quit to menu"
|
||||||
|
(:info colors)))
|
||||||
|
|
||||||
|
(screen/hide-cursor!)
|
||||||
|
(screen/redraw!))
|
||||||
|
|
||||||
|
(defn render-game-over
|
||||||
|
"Renders game over screen"
|
||||||
|
[game-state]
|
||||||
|
(screen/clear-screen!)
|
||||||
|
(let [[width height] (screen/get-size)
|
||||||
|
winner (:winner game-state)
|
||||||
|
winner-name (if (= winner :human) "PLAYER" "COMPUTER")]
|
||||||
|
(screen/put-string! (- (/ width 2) 10) (/ height 2)
|
||||||
|
(str "GAME OVER - " winner-name " WINS!")
|
||||||
|
(:title colors))
|
||||||
|
(screen/put-string! (- (/ width 2) 15) (inc (/ height 2))
|
||||||
|
"Press any key to return to menu"
|
||||||
|
(:info colors)))
|
||||||
|
(screen/hide-cursor!)
|
||||||
|
(screen/redraw!)
|
||||||
|
(screen/get-key))
|
||||||
58
src/knife_fight/ui/screen.clj
Normal file
58
src/knife_fight/ui/screen.clj
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
(ns knife-fight.ui.screen
|
||||||
|
(:require [lanterna.screen :as scr]))
|
||||||
|
|
||||||
|
(defonce screen-state (atom nil))
|
||||||
|
|
||||||
|
(defn init-screen!
|
||||||
|
"Initializes the Lanterna screen"
|
||||||
|
[]
|
||||||
|
(let [screen (scr/get-screen :text)]
|
||||||
|
(scr/start screen)
|
||||||
|
(reset! screen-state screen)
|
||||||
|
screen))
|
||||||
|
|
||||||
|
(defn stop-screen!
|
||||||
|
"Stops and cleans up the screen"
|
||||||
|
[]
|
||||||
|
(when @screen-state
|
||||||
|
(scr/stop @screen-state)
|
||||||
|
(reset! screen-state nil)))
|
||||||
|
|
||||||
|
(defn hide-cursor!
|
||||||
|
"Hides the terminal cursor by setting cursor position to null"
|
||||||
|
[]
|
||||||
|
(when @screen-state
|
||||||
|
(.setCursorPosition @screen-state nil)))
|
||||||
|
|
||||||
|
(defn clear-screen!
|
||||||
|
"Clears the screen"
|
||||||
|
[]
|
||||||
|
(when @screen-state
|
||||||
|
(scr/clear @screen-state)))
|
||||||
|
|
||||||
|
(defn redraw!
|
||||||
|
"Redraws the screen"
|
||||||
|
[]
|
||||||
|
(when @screen-state
|
||||||
|
(scr/redraw @screen-state)))
|
||||||
|
|
||||||
|
(defn get-key
|
||||||
|
"Gets a key press (blocking)"
|
||||||
|
[]
|
||||||
|
(when @screen-state
|
||||||
|
(scr/get-key-blocking @screen-state)))
|
||||||
|
|
||||||
|
(defn put-string!
|
||||||
|
"Puts a string at specified coordinates with optional styling"
|
||||||
|
([x y text]
|
||||||
|
(when @screen-state
|
||||||
|
(scr/put-string @screen-state x y text)))
|
||||||
|
([x y text styles]
|
||||||
|
(when @screen-state
|
||||||
|
(scr/put-string @screen-state x y text styles))))
|
||||||
|
|
||||||
|
(defn get-size
|
||||||
|
"Returns [width height] of terminal"
|
||||||
|
[]
|
||||||
|
(when @screen-state
|
||||||
|
(scr/get-size @screen-state)))
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
[{:dependencies {org.clojure/clojure {:vsn "1.11.1", :native-prefix nil}, org.clojure/spec.alpha {:vsn "0.3.218", :native-prefix nil}, org.clojure/core.specs.alpha {:vsn "0.2.62", :native-prefix nil}, clojure-lanterna {:vsn "0.9.7", :native-prefix nil}, com.googlecode.lanterna/lanterna {:vsn "2.1.7", :native-prefix nil}, nrepl {:vsn "1.0.0", :native-prefix nil}, org.nrepl/incomplete {:vsn "0.1.0", :native-prefix nil}}, :native-path "target/native"} {:native-path "target/native", :dependencies {org.clojure/clojure {:vsn "1.11.1", :native-prefix nil, :native? false}, org.clojure/spec.alpha {:vsn "0.3.218", :native-prefix nil, :native? false}, org.clojure/core.specs.alpha {:vsn "0.2.62", :native-prefix nil, :native? false}, clojure-lanterna {:vsn "0.9.7", :native-prefix nil, :native? false}, com.googlecode.lanterna/lanterna {:vsn "2.1.7", :native-prefix nil, :native? false}, nrepl {:vsn "1.0.0", :native-prefix nil, :native? false}, org.nrepl/incomplete {:vsn "0.1.0", :native-prefix nil, :native? false}}}]
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
artifactId=knife-fight
|
||||||
|
groupId=knife-fight
|
||||||
|
version=0.1.0-SNAPSHOT
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
[{:dependencies {org.clojure/clojure {:vsn "1.11.1", :native-prefix nil}, org.clojure/spec.alpha {:vsn "0.3.218", :native-prefix nil}, org.clojure/core.specs.alpha {:vsn "0.2.62", :native-prefix nil}, clojure-lanterna {:vsn "0.9.7", :native-prefix nil}, com.googlecode.lanterna/lanterna {:vsn "2.1.7", :native-prefix nil}, nrepl {:vsn "1.0.0", :native-prefix nil}, org.nrepl/incomplete {:vsn "0.1.0", :native-prefix nil}}, :native-path "target/native"} {:native-path "target/native", :dependencies {org.clojure/clojure {:vsn "1.11.1", :native-prefix nil, :native? false}, org.clojure/spec.alpha {:vsn "0.3.218", :native-prefix nil, :native? false}, org.clojure/core.specs.alpha {:vsn "0.2.62", :native-prefix nil, :native? false}, clojure-lanterna {:vsn "0.9.7", :native-prefix nil, :native? false}, com.googlecode.lanterna/lanterna {:vsn "2.1.7", :native-prefix nil, :native? false}, nrepl {:vsn "1.0.0", :native-prefix nil, :native? false}, org.nrepl/incomplete {:vsn "0.1.0", :native-prefix nil, :native? false}}}]
|
||||||
Loading…
Add table
Reference in a new issue