diff options
56 files changed, 17768 insertions, 310 deletions
diff --git a/bash/computer b/bash/computer deleted file mode 100755 index e5aa36d..0000000 --- a/bash/computer +++ /dev/null @@ -1,295 +0,0 @@ -#!/bin/bash - -# Get the directory where this script is located -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# Computer Dispatch System -# This script intelligently routes prompts to the most appropriate thinking mechanism -# or directly to Ollama based on complexity, question type, and user intent. -# -# APPLICATION LOGIC: -# The computer dispatch system implements an intelligent routing mechanism that -# analyzes user prompts and determines the optimal response strategy. The system -# operates through three distinct phases designed to maximize response quality: -# -# PHASE 1 - PROMPT ANALYSIS: -# - Analyzes prompt complexity, length, and question type -# - Identifies user intent and specific keywords -# - Determines if direct Ollama response is appropriate -# - Classifies prompts into response categories -# -# PHASE 2 - MECHANISM SELECTION: -# - Routes to appropriate thinking mechanism based on classification -# - Uses decision tree with keywords for clear cases -# - Considers prompt complexity and user intent -# - Falls back to direct Ollama for simple cases -# -# PHASE 3 - RESPONSE EXECUTION: -# - Executes the selected mechanism or direct Ollama call -# - Maintains transparency about the routing decision -# - Provides consistent output format regardless of mechanism -# - Logs the decision process for analysis -# -# DISPATCH MODELING: -# The system applies intelligent routing principles to AI response generation: -# - Prompt classification helps match complexity to appropriate mechanism -# - Keyword analysis identifies specific user needs and intent -# - Decision tree provides consistent, predictable routing logic -# - Direct Ollama routing handles simple cases efficiently -# - Transparency shows users how their prompt was processed -# - The system may improve response quality by using specialized mechanisms -# -# The dispatch process emphasizes efficiency and appropriateness, -# ensuring users get the best possible response for their specific needs. -# The system balances speed with depth based on prompt characteristics. - -# --- Model Configuration --- -DEFAULT_MODEL="gemma3n:e2b" - -# --- Defaults --- -DEFAULT_ROUNDS=2 - -# --- Argument Validation --- -if [ "$#" -lt 1 ]; then - echo -e "\n\tComputer" - echo -e "\tThis script intelligently routes prompts to the most appropriate thinking mechanism" - echo -e "\tor directly to Ollama based on complexity, question type, and user intent." - echo -e "\n\tUsage: $0 [-f <file_path>] [-d] \"<your prompt>\" [number_of_rounds]" - echo -e "\n\tExample: $0 -f ./input.txt \"Please analyze this text\" 2" - echo -e "\n\tIf number_of_rounds is not provided, the program will default to $DEFAULT_ROUNDS rounds." - echo -e "\n\t-f <file_path> (optional): Append the contents of the file to the prompt." - echo -e "\n\t-d (optional): Force direct Ollama response (bypass thinking mechanisms)." - echo -e "\n" - exit 1 -fi - -# --- Argument Parsing --- -FILE_PATH="" -FORCE_DIRECT=false -while getopts "f:d" opt; do - case $opt in - f) - FILE_PATH="$OPTARG" - ;; - d) - FORCE_DIRECT=true - ;; - *) - echo "Invalid option: -$OPTARG" >&2 - exit 1 - ;; - esac -done -shift $((OPTIND -1)) - -PROMPT="$1" -if [ -z "$2" ]; then - ROUNDS=$DEFAULT_ROUNDS -else - ROUNDS=$2 -fi - -# If file path is provided, append its contents to the prompt -if [ -n "$FILE_PATH" ]; then - if [ ! -f "$FILE_PATH" ]; then - echo "File not found: $FILE_PATH" >&2 - exit 1 - fi - FILE_CONTENTS=$(cat "$FILE_PATH") - PROMPT="$PROMPT\n[FILE CONTENTS]\n$FILE_CONTENTS\n[END FILE]" -fi - -# Source the logging system using absolute path -source "${SCRIPT_DIR}/logging.sh" - -# --- File Initialization --- -# Create a temporary directory if it doesn't exist -mkdir -p ~/tmp -# Create a unique file for this session based on the timestamp -SESSION_FILE=~/tmp/computer_$(date +%Y%m%d_%H%M%S).txt - -# Initialize timing -SESSION_ID=$(generate_session_id) -start_timer "$SESSION_ID" "computer" - -echo "Computer Dispatch Session Log: ${SESSION_FILE}" -echo "---------------------------------" - -# Store the initial user prompt in the session file -echo "USER PROMPT: ${PROMPT}" >> "${SESSION_FILE}" -echo "FORCE DIRECT: ${FORCE_DIRECT}" >> "${SESSION_FILE}" -echo "" >> "${SESSION_FILE}" - -# --- Prompt Analysis Function --- -analyze_prompt() { - local prompt="$1" - local analysis="" - - # Check for direct Ollama requests - if [[ "$prompt" =~ (direct|simple|quick|fast|straight) ]]; then - analysis="DIRECT" - return - fi - - # Check prompt length (simple heuristic for complexity) - local word_count=$(echo "$prompt" | wc -w) - - # Very short prompts (likely simple questions) - if [ "$word_count" -le 5 ]; then - analysis="DIRECT" - return - fi - - # Keyword-based classification - if [[ "$prompt" =~ (consensus|agree|disagree|vote|multiple|perspectives|opinions) ]]; then - analysis="CONSENSUS" - elif [[ "$prompt" =~ (synthesize|combine|integrate|unify|merge|consolidate) ]]; then - analysis="SYNTHESIS" - elif [[ "$prompt" =~ (explore|paths|alternatives|options|compare|strategies|approaches) ]]; then - analysis="EXPLORATION" - elif [[ "$prompt" =~ (analyze|examine|explore|investigate|deep|thorough|comprehensive) ]]; then - analysis="SOCRATIC" - elif [[ "$prompt" =~ (improve|refine|edit|revise|better|enhance|polish|fix) ]]; then - analysis="CRITIQUE" - elif [[ "$prompt" =~ (review|feedback|peer|collaborate|suggest|advice) ]]; then - analysis="PEER_REVIEW" - else - # Default to direct for unclear cases - analysis="DIRECT" - fi - - echo "$analysis" -} - -# --- Mechanism Selection --- -echo "Analyzing prompt and selecting mechanism..." -echo "PROMPT ANALYSIS:" >> "${SESSION_FILE}" - -if [ "$FORCE_DIRECT" = true ]; then - MECHANISM="DIRECT" - REASON="User requested direct response with -d flag" -else - MECHANISM=$(analyze_prompt "$PROMPT") - case "$MECHANISM" in - "DIRECT") - REASON="Simple prompt or direct request" - ;; - "CONSENSUS") - REASON="Multiple perspectives or consensus needed" - ;; - "SYNTHESIS") - REASON="Integration of multiple approaches needed" - ;; - "EXPLORATION") - REASON="Systematic exploration of alternatives needed" - ;; - "SOCRATIC") - REASON="Deep analysis or exploration required" - ;; - "CRITIQUE") - REASON="Improvement or refinement requested" - ;; - "PEER_REVIEW") - REASON="Collaborative review or feedback needed" - ;; - *) - REASON="Default fallback" - MECHANISM="DIRECT" - ;; - esac -fi - -echo "Selected mechanism: ${MECHANISM}" >> "${SESSION_FILE}" -echo "Reason: ${REASON}" >> "${SESSION_FILE}" -echo "" >> "${SESSION_FILE}" - -echo "Selected mechanism: ${MECHANISM}" -echo "Reason: ${REASON}" -echo "---------------------------------" - -# --- Response Execution --- -echo "Executing selected mechanism..." -echo "RESPONSE EXECUTION:" >> "${SESSION_FILE}" - -case "$MECHANISM" in - "DIRECT") - echo "Using direct Ollama response..." - echo "DIRECT OLLAMA RESPONSE:" >> "${SESSION_FILE}" - - DIRECT_PROMPT="You are an expert assistant. You always flag if you don't know something. Please provide a clear, helpful response to the following prompt: ${PROMPT}" - - RESPONSE=$(ollama run "${DEFAULT_MODEL}" "${DIRECT_PROMPT}") - - echo "${RESPONSE}" >> "${SESSION_FILE}" - echo "" >> "${SESSION_FILE}" - - echo "---------------------------------" - echo "Direct response:" - echo "---------------------------------" - echo "${RESPONSE}" - ;; - - "CONSENSUS") - echo "Delegating to consensus mechanism..." - echo "DELEGATING TO CONSENSUS:" >> "${SESSION_FILE}" - - # Execute consensus script and display output directly - "${SCRIPT_DIR}/consensus" "${PROMPT}" "${ROUNDS}" 2>&1 | tee -a "${SESSION_FILE}" - ;; - - "SOCRATIC") - echo "Delegating to Socratic mechanism..." - echo "DELEGATING TO SOCRATIC:" >> "${SESSION_FILE}" - - # Execute Socratic script and display output directly - "${SCRIPT_DIR}/socratic" "${PROMPT}" "${ROUNDS}" 2>&1 | tee -a "${SESSION_FILE}" - ;; - - "CRITIQUE") - echo "Delegating to critique mechanism..." - echo "DELEGATING TO CRITIQUE:" >> "${SESSION_FILE}" - - # Execute critique script and display output directly - "${SCRIPT_DIR}/critique" "${PROMPT}" "${ROUNDS}" 2>&1 | tee -a "${SESSION_FILE}" - ;; - - "PEER_REVIEW") - echo "Delegating to peer-review mechanism..." - echo "DELEGATING TO PEER_REVIEW:" >> "${SESSION_FILE}" - - # Execute peer-review script and display output directly - "${SCRIPT_DIR}/peer-review" "${PROMPT}" "${ROUNDS}" 2>&1 | tee -a "${SESSION_FILE}" - ;; - - "SYNTHESIS") - echo "Delegating to synthesis mechanism..." - echo "DELEGATING TO SYNTHESIS:" >> "${SESSION_FILE}" - - # Execute synthesis script and display output directly - "${SCRIPT_DIR}/synthesis" "${PROMPT}" "${ROUNDS}" 2>&1 | tee -a "${SESSION_FILE}" - ;; - - "EXPLORATION") - echo "Delegating to exploration mechanism..." - echo "DELEGATING TO EXPLORATION:" >> "${SESSION_FILE}" - - # Execute exploration script and display output directly - "${SCRIPT_DIR}/exploration" "${PROMPT}" "${ROUNDS}" 2>&1 | tee -a "${SESSION_FILE}" - ;; -esac - -# --- Final Summary --- -echo "" >> "${SESSION_FILE}" -echo "DISPATCH SUMMARY:" >> "${SESSION_FILE}" -echo "================" >> "${SESSION_FILE}" -echo "Original Prompt: ${PROMPT}" >> "${SESSION_FILE}" -echo "Selected Mechanism: ${MECHANISM}" >> "${SESSION_FILE}" -echo "Reason: ${REASON}" >> "${SESSION_FILE}" -echo "Rounds: ${ROUNDS}" >> "${SESSION_FILE}" - -# End timing -duration=$(end_timer "$SESSION_ID" "computer") - -echo "" -echo "Execution time: ${duration} seconds" -echo "Full dispatch log: ${SESSION_FILE}" \ No newline at end of file diff --git a/bash/talk-to-computer/README.md b/bash/talk-to-computer/README.md new file mode 100644 index 0000000..493a54c --- /dev/null +++ b/bash/talk-to-computer/README.md @@ -0,0 +1,876 @@ +# AI Thinking Mechanisms + +A Bash-based system that routes user prompts through different AI thinking mechanisms. It uses pattern matching and basic analysis to select from multiple approaches: direct response, socratic questioning, exploration, critique, consensus, synthesis, and puzzle solving. + +## Architecture Overview + +The system processes prompts through several stages: + +``` +User Prompt → Computer (Dispatcher) → Classifier → RAG System → Mechanism → LLM Models → Quality Guard → Response + ↓ ↓ ↓ ↓ ↓ ↓ ↓ + Validation Pattern Analysis Selection Corpus Search Processing Model Calls Quality Check + ↓ ↓ ↓ ↓ ↓ ↓ ↓ + Sanitization Keyword Matching Routing Context Augment Execution Fallbacks Error Handling +``` + +### Core Components + +- **`computer`** - Main dispatcher script with manual mechanism selection +- **`classifier.sh`** - Advanced prompt classification with Lil-specific routing +- **`logging.sh`** - Logging and validation utilities +- **`quality_guard.sh`** - System-wide response quality monitoring +- **`corpus/`** - RAG knowledge corpus directory structure +- **`corpus_manager.sh`** - Corpus management and auto-discovery +- **`rag_search.sh`** - Efficient corpus searching with Unix tools +- **Thinking Mechanisms** - Specialized AI interaction patterns +- **Dynamic Model Selection** - Intelligent model routing based on task type + +## Getting Started + +### Prerequisites + +- **Bash 4.0+** (for advanced features) +- **Ollama** installed and running +- **jq** (optional, for enhanced JSON processing) +- **bc** (optional, for precise timing calculations) + +### Installation + +1. Clone or download the scripts to your desired directory +2. Ensure all scripts have execute permissions: + ```bash + chmod +x computer exploration consensus socratic critique peer-review synthesis puzzle + chmod +x logging.sh classifier.sh quality_guard.sh + ``` +3. Verify Ollama is running and accessible: + ```bash + ollama list + ``` + +### Basic Usage + +```bash +# Use the intelligent dispatcher (recommended) +./computer "Your question or prompt here" + +# Force direct response (bypass thinking mechanisms) +./computer -d "Simple question" + +# Include file context +./computer -f input.txt "Analyze this file" + +# Specify number of rounds +./computer "Complex question" 3 + +# Manual mechanism selection +./computer -m puzzle "How can I implement a sorting algorithm?" +./computer -m socratic "Analyze this deeply" + +# Get help +./computer --help # Show all options and examples +./computer --mechanisms # List available thinking mechanisms +``` + +## Quality Guard System + +### Basic Response Quality Monitoring + +The Quality Guard provides simple monitoring and error handling for AI responses. + +#### **What It Does** + +- Monitors basic response characteristics +- Detects obvious quality issues +- Provides fallback responses when needed +- Attempts to regenerate responses up to 2 times + +#### **How It Works** + +1. **Basic Checks** - Simple pattern matching for obvious issues +2. **Scoring** - Basic heuristics for response quality +3. **Retry Logic** - Up to 2 attempts to get better responses +4. **Fallbacks** - Generic helpful responses when retries fail + +#### **Limitations** + +- Uses simple pattern matching, not advanced analysis +- May not catch subtle quality issues +- Fallback responses are generic +- Not a substitute for careful prompt engineering + +#### **Configuration Options** + +```bash +# Basic threshold adjustments +export MIN_RESPONSE_LENGTH=30 # Minimum words required +export MAX_REPETITION_RATIO=0.4 # Maximum repetition allowed +export MAX_NONSENSE_SCORE=0.6 # Maximum nonsense score +export DEGRADATION_THRESHOLD=0.65 # Quality threshold for correction +export MAX_CORRECTION_ATTEMPTS=2 # Number of correction attempts +export FALLBACK_ENABLED=true # Enable fallback responses +``` + +## RAG (Retrieval-Augmented Generation) System + +### Knowledge Corpus Architecture + +The RAG system provides intelligent knowledge augmentation by searching a structured corpus of documentation and returning relevant context to enhance AI responses. + +#### **Key Features** + +- **Extensible Corpus Structure** - Easy to add new topics and content +- **Efficient Search** - Uses grep/sed/awk for sub-second lookups +- **Auto-Discovery** - Automatically finds and indexes new content +- **Topic-Based Routing** - Matches queries to relevant knowledge areas +- **Context Injection** - Provides relevant information to AI models + +#### **Corpus Organization** + +``` +corpus/ +├── README.md # Usage guide and templates +├── corpus_registry.txt # Auto-generated registry of topics +├── corpus_manager.sh # Management utilities +├── .topic_keywords # Topic keyword mappings +├── .file_processors # File type handlers +│ +├── programming/ # Programming topics +│ ├── lil/ # Lil programming language +│ │ └── guide.md +│ └── algorithms.txt +│ +├── science/ # Scientific topics +│ ├── physics.txt +│ └── chemistry.md +│ +└── [your_topics]/ # Add your own topics here +``` + +#### **Corpus Manager Usage** + +```bash +# Update corpus registry after adding files +./corpus_manager.sh update + +# List all available topics +./corpus_manager.sh list + +# Check if topic exists +./corpus_manager.sh exists programming + +# List files in a topic +./corpus_manager.sh files science + +# Create template for new topic +./corpus_manager.sh template "machine-learning" + +# Get corpus statistics +./corpus_manager.sh count programming +``` + +#### **RAG Search Usage** + +```bash +# Search entire corpus +./rag_search.sh search "quantum physics" + +# Search specific topic +./rag_search.sh search "lil programming" programming + +# Get context around matches +./rag_search.sh context "variables" programming + +# Extract relevant sections +./rag_search.sh extract "functions" programming + +# Show corpus statistics +./rag_search.sh stats +``` + +#### **Adding New Content** + +1. **Create topic directory**: + ```bash + mkdir -p corpus/newtopic + ``` + +2. **Add content files** (use .md, .txt, or .html): + ```bash + vim corpus/newtopic/guide.md + vim corpus/newtopic/examples.txt + ``` + +3. **Update registry**: + ```bash + ./corpus_manager.sh update + ``` + +4. **Test search**: + ```bash + ./rag_search.sh search "keyword" newtopic + ``` + +#### **File Format Guidelines** + +- **Markdown (.md)** - Recommended for structured content +- **Plain text (.txt)** - Simple notes and documentation +- **HTML (.html)** - Rich content with formatting +- **Descriptive names** - Use clear, descriptive filenames +- **Consistent headers** - Use standard Markdown headers (# ## ###) +- **Cross-references** - Link related topics when helpful + +#### **Search Behavior** + +- **Case-insensitive** matching across all text files +- **Multi-word queries** supported +- **Partial matching** within words +- **Context extraction** with configurable line limits +- **Topic filtering** for focused searches +- **Relevance ranking** based on match proximity + +#### **Integration with AI** + +The RAG system integrates seamlessly with thinking mechanisms: + +- **Automatic RAG detection** - Knows when to search corpus +- **Topic classification** - Routes queries to relevant knowledge +- **Context injection** - Provides relevant information to enhance responses +- **Fallback handling** - Graceful degradation when no corpus available + +#### **Performance** + +- **Sub-second lookups** using cached registry +- **Efficient Unix tools** (grep/sed/awk) for processing +- **Memory efficient** with file-based storage +- **Scalable architecture** supporting thousands of files +- **Minimal latency** for AI response enhancement + +#### **Configuration** + +```bash +# RAG system settings (in rag_config.sh) +export CORPUS_DIR="corpus" # Corpus root directory +export CORPUS_REGISTRY="corpus_registry.txt" # Topic registry +export MAX_SEARCH_RESULTS=5 # Max results to return +export MIN_CONTENT_LENGTH=50 # Min content length +export SEARCH_CONTEXT_LINES=3 # Context lines around matches +``` + +## Prompt Classification + +### Basic Pattern Matching + +The system uses keyword and pattern matching to route prompts to different mechanisms: + +#### **Pattern Matching Rules** +- **Question type detection**: what/when/where → DIRECT, why/how → SOCRATIC +- **Action-oriented patterns**: improve → CRITIQUE, compare → EXPLORATION +- **Puzzle & coding patterns**: algorithm/implement → PUZZLE, challenge/problem → PUZZLE +- **Lil-specific routing**: "using lil"/"in lil" → PUZZLE (highest priority) +- **Context-aware scoring**: strategy/planning → EXPLORATION, analysis → SOCRATIC +- **Enhanced scoring system** with multi-layer analysis + +#### **Basic Analysis** +- **Word count analysis**: Short prompts → DIRECT, longer → complex mechanisms +- **Keyword presence**: Simple keyword matching for routing decisions +- **Basic confidence scoring**: Simple scoring mechanism + +#### **Limitations** +- Relies on keyword matching, not deep understanding +- May misclassify prompts without obvious keywords +- Confidence scores are basic heuristics, not accurate measures +- Not a substitute for manual routing when precision matters + +### Decision Making + +- **Basic confidence scoring** from pattern matching +- **Keyword-based routing** with fallback to DIRECT +- **Simple word count analysis** for complexity estimation + +### Classification Examples + +```bash +# Strategic Planning +Input: "What are the different approaches to solve climate change?" +Output: EXPLORATION:1.00 (matches "different approaches" pattern) + +# Improvement Request +Input: "How can we improve our development workflow?" +Output: CRITIQUE:1.00 (matches "improve" keyword) + +# Complex Analysis +Input: "Why do you think this approach might fail and what are the underlying assumptions?" +Output: SOCRATIC:0.85 (matches "why" and complexity indicators) + +# Simple Question +Input: "What is 2+2?" +Output: DIRECT:0.8 (simple, short question) + +# Algorithm Challenge +Input: "How can I implement a binary search algorithm?" +Output: PUZZLE:1.00 (matches "algorithm" and "implement" keywords) +``` + +## Thinking Mechanisms + +### 1. **Exploration** - Multiple Path Analysis +**Purpose**: Generate multiple solution approaches and compare them + +```bash +./exploration -p 4 "How can we improve our development process?" +``` + +**Process**: +- **Phase 1**: Generate multiple solution paths +- **Phase 2**: Basic analysis of each path +- **Phase 3**: Simple comparison and recommendations + +**Notes**: Uses multiple LLM calls to generate different approaches + +### 2. **Consensus** - Multiple Model Responses +**Purpose**: Get responses from multiple models and compare them + +```bash +./consensus "What's the best approach to this problem?" +``` + +**Process**: +- **Phase 1**: Get responses from multiple models +- **Phase 2**: Basic comparison +- **Phase 3**: Simple voting mechanism +- **Phase 4**: Combine responses + +**Notes**: Limited by available models and simple comparison logic + +### 3. **Socratic** - Question-Based Analysis +**Purpose**: Use AI-generated questions to analyze prompts + +```bash +./socratic "Explain the implications of this decision" +``` + +**Process**: +- **Phase 1**: Generate initial response +- **Phase 2**: Generate follow-up questions +- **Phase 3**: Get responses to questions +- **Phase 4**: Combine into final output + +**Notes**: Creates a back-and-forth conversation between AI models + +### 4. **Critique** - Improvement Analysis +**Purpose**: Get improvement suggestions for code or text + +```bash +./critique -f code.py "How can we improve this code?" +``` + +**Process**: +- **Phase 1**: Initial assessment +- **Phase 2**: Generate critique +- **Phase 3**: Suggest improvements +- **Phase 4**: Provide guidance + +**Notes**: Basic improvement suggestions based on AI analysis + +### 5. **Peer Review** - Multiple AI Reviewers +**Purpose**: Get feedback from multiple AI perspectives + +```bash +./peer-review "Review this proposal" +``` + +**Process**: +- **Phase 1**: Generate multiple reviews +- **Phase 2**: Basic consolidation +- **Phase 3**: Combine feedback + +**Notes**: Simple multiple AI review approach + +### 6. **Synthesis** - Combine Approaches +**Purpose**: Combine multiple approaches into one + +```bash +./synthesis "How can we combine these different approaches?" +``` + +**Process**: +- **Phase 1**: Identify approaches +- **Phase 2**: Basic analysis +- **Phase 3**: Simple combination + +**Notes**: Basic approach combination mechanism + +### 7. **Puzzle** - Coding Problem Solving +**Purpose**: Help with coding problems and algorithms + +```bash +./puzzle "How can I implement a sorting algorithm?" +./puzzle -l python "What's the best way to solve this data structure problem?" +``` + +**Process**: +- **Phase 1**: Basic problem analysis +- **Phase 2**: Solution approach +- **Phase 3**: Code examples +- **Phase 4**: Basic validation +- **Phase 5**: Optional Lil code testing if available + +**Features**: +- **Enhanced Lil language knowledge** with comprehensive documentation +- **Intelligent Lil routing** - automatically triggered by Lil-related keywords +- **RAG integration** - searches Lil corpus for relevant context +- **Code testing** with secure Lil script execution +- **Multi-language support** with Lil as primary focus + +**Notes**: Includes extensive Lil documentation and testing capabilities + +## Computer Script Features + +### Intelligent Routing with Manual Override + +The main `computer` script provides both automatic routing and manual mechanism selection: + +#### **Automatic Routing** +```bash +# Automatically detects and routes based on content +./computer "Using Lil, how can I implement a sorting algorithm?" +# → PUZZLE (Lil-specific routing) + +./computer "How can we improve our development process?" +# → EXPLORATION (improvement keywords) + +./computer "What is 2+2?" +# → DIRECT (simple question) +``` + +#### **Manual Selection** +```bash +# Force specific mechanism +./computer -m puzzle "Complex algorithm question" +./computer -m socratic "Deep analysis needed" +./computer -m exploration "Compare multiple approaches" +./computer -m consensus "Get multiple perspectives" +./computer -m critique "Review and improve" +./computer -m synthesis "Combine different ideas" +./computer -m peer-review "Get feedback" +./computer -m direct "Simple factual question" +``` + +#### **Help System** +```bash +# Comprehensive help +./computer --help + +# List all available mechanisms +./computer --mechanisms +``` + +### Advanced Options + +```bash +# File integration +./computer -f document.txt "Analyze this content" + +# Multi-round processing +./computer "Complex topic" 3 + +# Force direct response (bypass mechanisms) +./computer -d "Simple question" +``` + +### Routing Intelligence + +The computer script uses multi-layer classification: +- **Pattern Analysis**: Keyword and pattern matching +- **Semantic Analysis**: LLM-based content understanding +- **Complexity Assessment**: Word count and structure analysis +- **Lil-Specific Routing**: Automatic PUZZLE for Lil-related queries +- **Confidence Scoring**: Ensures high-confidence routing decisions + +## Configuration + +### Model Selection + +### Dynamic Model Selection (NEW) + +The system now includes intelligent model selection based on task type and model capabilities: + +```bash +# Enable dynamic selection +source model_selector.sh +selected_model=$(select_model_for_task "How can I implement a sorting algorithm?" "puzzle" "") +echo "Selected: $selected_model" +``` + +**Features:** +- **Task-aware selection**: Matches models to coding, reasoning, or creative tasks +- **Capability scoring**: Rates models by performance in different areas (0.0-1.0) +- **Real-time discovery**: Automatically finds available models via Ollama +- **Performance weighting**: Considers speed, size, and capability scores +- **Fallback handling**: Graceful degradation when preferred models unavailable + +**Model Capabilities Database:** +- `llama3:8b-instruct-q4_K_M`: Excellent reasoning (0.9), good coding (0.8) +- `phi3:3.8b-mini-4k-instruct-q4_K_M`: Fast (0.9 speed), good reasoning (0.8) +- `gemma3n:e2b`: Balanced performer (0.8 across all categories) +- `deepseek-r1:1.5b`: Excellent reasoning (0.9), fast (0.95 speed) + +### Static Model Selection (Legacy) + +For simple setups, you can still use static model configuration: + +```bash +# Models for different mechanisms +EXPLORATION_MODEL="llama3:8b-instruct-q4_K_M" +ANALYSIS_MODEL="phi3:3.8b-mini-4k-instruct-q4_K_M" + +# Models for consensus mechanism +MODELS=( + "llama3:8b-instruct-q4_K_M" + "phi3:3.8b-mini-4k-instruct-q4_K_M" + "deepseek-r1:1.5b" + "gemma3n:e2b" + "dolphin3:latest" +) +``` + +### Model Management + +The system includes both basic and advanced model management: + +- **Availability checking**: Verifies models are available before use +- **Fallback mechanisms**: Automatic fallback to alternative models +- **Error handling**: Graceful handling of model unavailability +- **Performance tracking**: Optional model performance history + +## Logging & Metrics + +### Session Logging + +All sessions are logged with comprehensive metadata: +- **Timing information** (start/end/duration) +- **Input validation** results +- **Classification decisions** with confidence scores +- **Model selection** decisions +- **Full conversation** transcripts +- **Error handling** details +- **Quality monitoring** results and correction attempts + +### Performance Metrics + +```bash +# View performance summary +get_metrics_summary + +# Metrics stored in JSON format +~/tmp/ai_thinking/performance_metrics.json +``` + +### Error Logging + +```bash +# Error log location +~/tmp/ai_thinking/errors.log + +# Warning log location +~/tmp/ai_thinking/errors.log + +# Classification logs +~/tmp/ai_thinking/classification.log + +# Quality monitoring logs (integrated into session files) +``` + +## Security & Validation + +### Input Sanitization + +- **Prompt length** validation (max 10,000 characters) +- **Special character** sanitization with warnings +- **File path** validation and security checks +- **Parameter** bounds checking (rounds: 1-5, paths: 1-10) + +### Error Handling + +- **Graceful degradation** when models unavailable +- **Comprehensive error** logging and reporting +- **User-friendly** error messages with actionable guidance +- **Fallback mechanisms** for critical failures +- **Input validation** with clear error reporting +- **Quality degradation** protection with automatic correction + +## Advanced Features + +### File Integration + +```bash +# Include file contents in prompts +./computer -f document.txt "Analyze this document" + +# File validation and security +- Path existence checking +- Read permission validation +- Content sanitization +- Graceful error handling +``` + +### Multi-Round Processing + +```bash +# Specify processing rounds (1-5) +./computer "Complex question" 3 + +# Each round builds on previous insights +- Round 1: Initial analysis +- Round 2: Deep exploration +- Round 3: Synthesis and conclusions +``` + +### Intelligent Routing + +The `computer` script automatically routes prompts based on: +- **Advanced classification** with confidence scoring +- **Multi-layer analysis** (pattern + semantic + complexity) +- **Context-aware** mechanism selection +- **Optimal mechanism** selection with fallbacks + +### Quality Monitoring Integration + +All thinking mechanisms now include: +- **Automatic quality assessment** of every LLM response +- **Degradation detection** with pattern recognition +- **Automatic correction** attempts for poor quality outputs +- **Intelligent fallbacks** when correction fails +- **Mechanism-specific** quality relevance checking + +## Testing & Validation + +### Validation Functions + +```bash +# Prompt validation +validate_prompt "Your prompt here" + +# File validation +validate_file_path "/path/to/file" + +# Model validation +validate_model "model_name" "fallback_model" + +# Classification testing +classify_prompt "Test prompt" false # Pattern-only mode +classify_prompt "Test prompt" true # Full semantic mode + +# Quality monitoring testing +assess_quality "response" "context" "mechanism" +detect_degradation_patterns "response" +guard_output_quality "response" "context" "mechanism" "model" +``` + +### Error Testing + +```bash +# Test with invalid inputs +./computer "" # Empty prompt +./computer -f nonexistent.txt # Missing file +./computer -x # Invalid option +./computer "test" 10 # Invalid rounds + +# Test quality monitoring +./test_quality_guard.sh # Quality guard system test +``` + +## Performance Considerations + +### Optimization Features + +- **Model availability** checking before execution +- **Efficient file** handling and validation +- **Minimal overhead** for simple queries +- **Classification caching** for repeated patterns +- **Parallel processing** where applicable +- **Quality monitoring** with minimal performance impact + +### Resource Management + +- **Temporary file** cleanup +- **Session management** with unique IDs +- **Memory-efficient** processing +- **Graceful timeout** handling +- **Classification result** caching +- **Quality assessment** optimization + +## Troubleshooting + +### Common Issues + +1. **"Ollama not found"** + - Ensure Ollama is installed and in PATH + - Check if Ollama service is running + +2. **"Model not available"** + - Verify model names with `ollama list` + - Check model download status + - System will automatically fall back to available models + +3. **"Permission denied"** + - Ensure scripts have execute permissions + - Check file ownership and permissions + +4. **"File not found"** + - Verify file path is correct + - Check file exists and is readable + - Ensure absolute or correct relative paths + +5. **"Low classification confidence"** + - Check error logs for classification details + - Consider using -d flag for direct responses + - Review prompt clarity and specificity + +6. **"Quality below threshold"** + - System automatically attempts correction + - Check quality monitoring logs for details + - Fallback responses ensure helpful output + - Consider rephrasing complex prompts + +### Debug Mode + +```bash +# Enable verbose logging +export AI_THINKING_DEBUG=1 + +# Check logs +tail -f ~/tmp/ai_thinking/errors.log + +# Test classification directly +source classifier.sh +classify_prompt "Your test prompt" true + +# Test quality monitoring +source quality_guard.sh +assess_quality "test response" "test context" "puzzle" +``` + +## Future Improvements + +### Possible Enhancements + +- Additional thinking mechanisms +- More model integration options +- Improved caching +- Better error handling +- Enhanced testing +- Performance optimizations + +### Extensibility + +The basic modular structure allows for: +- Adding new mechanisms +- Integrating additional models +- Modifying validation rules +- Extending logging +- Updating classification patterns + +## Examples + +### Strategic Planning + +```bash +./exploration -p 5 "What are our options for scaling this application?" +``` + +### Code Review + +```bash +./critique -f main.py "How can we improve this code's performance and maintainability?" +``` + +### Decision Making + +```bash +./consensus "Should we migrate to a new database system?" +``` + +### Problem Analysis + +```bash +./socratic "What are the root causes of our deployment failures?" +``` + +### Algorithm & Coding Challenges + +```bash +# The system automatically routes to puzzle mechanism +./computer "How can I implement a binary search algorithm?" +./puzzle "What's the most efficient way to sort this data structure?" +``` + +### Complex Classification + +```bash +# The system automatically detects this needs exploration +./computer "Compare different approaches to implementing microservices" +``` + +### Quality Monitoring in Action + +```bash +# All mechanisms now include quality protection +./computer "Complex question requiring deep analysis" 3 +# Quality monitoring automatically protects against degradation +# Fallback responses ensure helpful output even with poor LLM performance +``` + +### RAG-Enhanced Responses + +```bash +# Lil-specific questions automatically use RAG +./computer "Using Lil, how can I implement a recursive function?" +# → PUZZLE with Lil knowledge corpus context + +# Manual corpus search +./rag_search.sh search "function definition" programming + +# Corpus management +./corpus_manager.sh template "data-structures" +./corpus_manager.sh update +``` + +### Manual Mechanism Selection + +```bash +# Force specific thinking style +./computer -m socratic "What are the fundamental assumptions here?" +./computer -m exploration "What are our strategic options?" +./computer -m puzzle "How can I optimize this algorithm?" + +# Get help with available options +./computer --mechanisms +``` + +## Contributing + +### Development Guidelines + +- **Maintain modularity** - Each mechanism should be self-contained +- **Follow error handling** patterns established in logging.sh +- **Add comprehensive** documentation for new features +- **Include validation** for all inputs and parameters +- **Test thoroughly** with various input types and edge cases +- **Update classification** patterns when adding new mechanisms +- **Integrate quality monitoring** for all new LLM interactions +- **Follow quality guard** patterns for consistent protection +- **Consider RAG integration** for domain-specific knowledge +- **Add corpus documentation** when extending knowledge areas +- **Update classification patterns** for new routing logic + +### Code Style + +- Consistent naming conventions +- Clear comments explaining complex logic +- Error handling for all external calls +- Modular functions with single responsibilities +- Validation functions for all inputs +- Comprehensive logging for debugging +- Quality monitoring integration for all AI responses \ No newline at end of file diff --git a/bash/talk-to-computer/classifier.sh b/bash/talk-to-computer/classifier.sh new file mode 100755 index 0000000..38f4869 --- /dev/null +++ b/bash/talk-to-computer/classifier.sh @@ -0,0 +1,281 @@ +#!/bin/bash + +# Advanced Prompt Classification System +# Multi-layer approach combining semantic analysis, pattern matching, and confidence scoring + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/logging.sh" + +# --- Classification Configuration --- +CLASSIFIER_MODEL="gemma3n:e2b" # Lightweight model for classification +CONFIDENCE_THRESHOLD=0.7 + +# --- Semantic Classification --- +classify_semantic() { + local prompt="$1" + + local classification_prompt="You are a prompt classifier. Analyze this prompt and determine which AI thinking mechanism would be most appropriate. + +PROMPT: \"$prompt\" + +AVAILABLE MECHANISMS: +- DIRECT: Simple questions, factual queries, straightforward requests +- CONSENSUS: Multiple perspectives needed, voting, agreement/disagreement +- SYNTHESIS: Combining approaches, integration, unification +- EXPLORATION: Comparing alternatives, strategic planning, option analysis +- SOCRATIC: Deep analysis, questioning assumptions, thorough investigation +- CRITIQUE: Improvement suggestions, refinement, enhancement +- PEER_REVIEW: Collaborative feedback, review processes, advice + +Respond with ONLY the mechanism name and confidence (0.0-1.0): +Format: MECHANISM_NAME:CONFIDENCE + +Example: EXPLORATION:0.85" + + local result=$(ollama run "$CLASSIFIER_MODEL" "$classification_prompt" 2>/dev/null) + echo "$result" +} + +# --- Intent Pattern Analysis --- +analyze_intent_patterns() { + local prompt="$1" + + # Initialize scores using individual variables (more portable) + local direct_score=0 + local consensus_score=0 + local synthesis_score=0 + local exploration_score=0 + local socratic_score=0 + local critique_score=0 + local peer_review_score=0 + local puzzle_score=0 + + # Question type patterns + if [[ "$prompt" =~ ^(what|when|where|who|which|how\ much|how\ many) ]]; then + direct_score=$((direct_score + 3)) + fi + + if [[ "$prompt" =~ ^(why|how|explain) ]]; then + socratic_score=$((socratic_score + 2)) + fi + + # Action-oriented patterns + if [[ "$prompt" =~ (compare|contrast|evaluate|assess) ]]; then + exploration_score=$((exploration_score + 3)) + fi + + if [[ "$prompt" =~ (improve|enhance|fix|refine|optimize|better) ]]; then + critique_score=$((critique_score + 3)) + fi + + if [[ "$prompt" =~ (combine|merge|integrate|synthesize|unify) ]]; then + synthesis_score=$((synthesis_score + 3)) + fi + + if [[ "$prompt" =~ (review|feedback|opinion|thoughts|suggest) ]]; then + peer_review_score=$((peer_review_score + 2)) + fi + + if [[ "$prompt" =~ (consensus|vote|agree|disagree|multiple.*view) ]]; then + consensus_score=$((consensus_score + 3)) + fi + + # Context patterns + if [[ "$prompt" =~ (strategy|strategic|plan|approach|option|alternative) ]]; then + exploration_score=$((exploration_score + 2)) + fi + + if [[ "$prompt" =~ (analyze|analysis|examine|investigate|deep|thorough) ]]; then + socratic_score=$((socratic_score + 2)) + fi + + # Puzzle and coding patterns + if [[ "$prompt" =~ (puzzle|solve|algorithm|code|programming|implement|sort|search|optimize|data.*structure) ]]; then + puzzle_score=$((puzzle_score + 3)) + fi + + if [[ "$prompt" =~ (challenge|problem|question|task|assignment|exercise) ]]; then + puzzle_score=$((puzzle_score + 2)) + fi + + # Lil-specific patterns - highest priority for puzzle mechanism + if [[ "$prompt" =~ (lil|LIL|using lil|in lil|with lil|lil programming|lil language|lil script) ]]; then + puzzle_score=$((puzzle_score + 5)) # Higher score than other patterns + fi + + # Find highest scoring mechanism + local max_score=0 + local best_mechanism="DIRECT" + + if [ "$direct_score" -gt "$max_score" ]; then + max_score="$direct_score" + best_mechanism="DIRECT" + fi + if [ "$consensus_score" -gt "$max_score" ]; then + max_score="$consensus_score" + best_mechanism="CONSENSUS" + fi + if [ "$synthesis_score" -gt "$max_score" ]; then + max_score="$synthesis_score" + best_mechanism="SYNTHESIS" + fi + if [ "$exploration_score" -gt "$max_score" ]; then + max_score="$exploration_score" + best_mechanism="EXPLORATION" + fi + if [ "$socratic_score" -gt "$max_score" ]; then + max_score="$socratic_score" + best_mechanism="SOCRATIC" + fi + if [ "$critique_score" -gt "$max_score" ]; then + max_score="$critique_score" + best_mechanism="CRITIQUE" + fi + if [ "$peer_review_score" -gt "$max_score" ]; then + max_score="$peer_review_score" + best_mechanism="PEER_REVIEW" + fi + if [ "$puzzle_score" -gt "$max_score" ]; then + max_score="$puzzle_score" + best_mechanism="PUZZLE" + fi + + # Calculate confidence based on score distribution + local total_score=$((direct_score + consensus_score + synthesis_score + exploration_score + socratic_score + critique_score + peer_review_score + puzzle_score)) + + local confidence="0.0" + if [ "$total_score" -gt 0 ]; then + confidence=$(echo "scale=2; $max_score / $total_score" | bc -l 2>/dev/null || echo "0.5") + fi + + echo "$best_mechanism:$confidence" +} + +# --- Complexity Analysis --- +analyze_complexity() { + local prompt="$1" + local word_count=$(echo "$prompt" | wc -w) + local sentence_count=$(echo "$prompt" | tr '.' '\n' | wc -l) + local question_count=$(echo "$prompt" | grep -o '?' | wc -l) + + # Simple heuristics for complexity + local complexity_score=0 + + # Word count factor + if [ "$word_count" -gt 50 ]; then + complexity_score=$((complexity_score + 3)) + elif [ "$word_count" -gt 20 ]; then + complexity_score=$((complexity_score + 2)) + elif [ "$word_count" -le 5 ]; then + complexity_score=$((complexity_score - 2)) + fi + + # Multiple questions suggest complexity + if [ "$question_count" -gt 1 ]; then + complexity_score=$((complexity_score + 2)) + fi + + # Multiple sentences suggest complexity + if [ "$sentence_count" -gt 3 ]; then + complexity_score=$((complexity_score + 1)) + fi + + echo "$complexity_score" +} + +# --- Confidence Weighted Classification --- +classify_prompt() { + local prompt="$1" + local use_semantic="${2:-true}" + + echo "=== Advanced Prompt Classification ===" >&2 + echo "Analyzing: \"$prompt\"" >&2 + echo >&2 + + # Get pattern-based classification + local pattern_result=$(analyze_intent_patterns "$prompt") + local pattern_mechanism=$(echo "$pattern_result" | cut -d':' -f1) + local pattern_confidence=$(echo "$pattern_result" | cut -d':' -f2) + + echo "Pattern Analysis: $pattern_mechanism (confidence: $pattern_confidence)" >&2 + + # Get complexity score + local complexity=$(analyze_complexity "$prompt") + echo "Complexity Score: $complexity" >&2 + + # Apply complexity adjustments + if [ "$complexity" -lt 0 ] && [ "$pattern_mechanism" != "DIRECT" ]; then + echo "Low complexity detected - suggesting DIRECT" >&2 + pattern_mechanism="DIRECT" + pattern_confidence="0.8" + elif [ "$complexity" -gt 4 ]; then + echo "High complexity detected - boosting complex mechanisms" >&2 + case "$pattern_mechanism" in + "DIRECT") + pattern_mechanism="SOCRATIC" + pattern_confidence="0.7" + ;; + esac + fi + + local final_mechanism="$pattern_mechanism" + local final_confidence="$pattern_confidence" + + # Use semantic classification if available and requested + if [ "$use_semantic" = "true" ] && command -v ollama >/dev/null 2>&1; then + echo "Running semantic analysis..." >&2 + local semantic_result=$(classify_semantic "$prompt") + + # Clean up the result + semantic_result=$(echo "$semantic_result" | tr -d ' ' | head -n1) + + if [[ "$semantic_result" =~ ^[A-Z_]+:[0-9.]+$ ]]; then + local semantic_mechanism=$(echo "$semantic_result" | cut -d':' -f1) + local semantic_confidence=$(echo "$semantic_result" | cut -d':' -f2) + + echo "Semantic Analysis: $semantic_mechanism (confidence: $semantic_confidence)" >&2 + + # Weighted combination of pattern and semantic results + local pattern_weight=$(echo "$pattern_confidence * 0.6" | bc -l 2>/dev/null || echo "0.3") + local semantic_weight=$(echo "$semantic_confidence * 0.4" | bc -l 2>/dev/null || echo "0.2") + + # If both agree, boost confidence + if [ "$pattern_mechanism" = "$semantic_mechanism" ]; then + final_confidence=$(echo "$pattern_confidence + 0.2" | bc -l 2>/dev/null || echo "0.8") + if (( $(echo "$final_confidence > 1.0" | bc -l 2>/dev/null || echo "0") )); then + final_confidence="1.0" + fi + echo "Pattern and semantic agree - boosting confidence to $final_confidence" >&2 + # If semantic has higher confidence, use it + elif (( $(echo "$semantic_confidence > $pattern_confidence + 0.1" | bc -l 2>/dev/null || echo "0") )); then + final_mechanism="$semantic_mechanism" + final_confidence="$semantic_confidence" + echo "Using semantic result due to higher confidence" >&2 + fi + else + log_warning "Semantic classification failed or returned invalid format: $semantic_result" + fi + fi + + # Final confidence check + if (( $(echo "$final_confidence < $CONFIDENCE_THRESHOLD" | bc -l 2>/dev/null || echo "0") )); then + echo "Low confidence ($final_confidence < $CONFIDENCE_THRESHOLD) - defaulting to DIRECT" >&2 + final_mechanism="DIRECT" + final_confidence="0.5" + fi + + echo >&2 + echo "=== Final Classification ===" >&2 + echo "Mechanism: $final_mechanism" >&2 + echo "Confidence: $final_confidence" >&2 + echo "============================" >&2 + + echo "$final_mechanism:$final_confidence" +} + +# --- Export Functions --- +export -f classify_prompt +export -f analyze_intent_patterns +export -f analyze_complexity +export -f classify_semantic diff --git a/bash/talk-to-computer/common.sh b/bash/talk-to-computer/common.sh new file mode 100755 index 0000000..4f11ffe --- /dev/null +++ b/bash/talk-to-computer/common.sh @@ -0,0 +1,234 @@ +#!/bin/bash + +# Common functionality shared across all AI thinking mechanisms +# This file contains utilities and initialization code used by multiple scripts + +# --- Script Directory Setup --- +# Get the directory where this script is located +get_script_dir() { + echo "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +} + +# --- Initialization Functions --- + +# Initialize a thinking mechanism script with common dependencies +init_thinking_mechanism() { + local script_path="$1" + + # Set up script directory + SCRIPT_DIR="$(cd "$(dirname "${script_path}")" && pwd)" + + # Source configuration + source "${SCRIPT_DIR}/config.sh" + + # Source logging system + source "${SCRIPT_DIR}/logging.sh" + + # Source quality guard for output quality protection + source "${SCRIPT_DIR}/quality_guard.sh" + + # Get mechanism name automatically + MECHANISM_NAME=$(get_mechanism_name "$script_path") + + # Set up resource management + setup_cleanup_trap + + export SCRIPT_DIR MECHANISM_NAME +} + +# Initialize the main dispatcher with common dependencies +init_dispatcher() { + local script_path="$1" + + # Set up script directory + SCRIPT_DIR="$(cd "$(dirname "${script_path}")" && pwd)" + + # Source logging system (dispatcher sources this later) + # source "${SCRIPT_DIR}/logging.sh" + + export SCRIPT_DIR +} + +# --- Model Validation Functions --- + +# Validate and set a model with fallback +validate_and_set_model() { + local model_var="$1" + local model_name="$2" + local fallback_model="$3" + + local validated_model + validated_model=$(validate_model "$model_name" "$fallback_model") + + if [ $? -ne 0 ]; then + log_error "No valid model available for $model_var" + return 1 + fi + + eval "$model_var=\"$validated_model\"" + echo "Set $model_var to: $validated_model" +} + +# --- Argument Processing Functions --- + +# Common file path validation +validate_file_arg() { + local file_path="$1" + + if [ -n "$file_path" ]; then + validate_file_path "$file_path" + if [ $? -ne 0 ]; then + return 1 + fi + fi + + echo "$file_path" +} + +# --- Cleanup Functions --- + +# Global array to track resources for cleanup +declare -a CLEANUP_RESOURCES=() + +# Register a resource for cleanup +register_cleanup_resource() { + local resource="$1" + CLEANUP_RESOURCES+=("$resource") +} + +# Clean up temporary resources +cleanup_resources() { + local exit_code=$? + + # Clean up registered resources + for resource in "${CLEANUP_RESOURCES[@]}"; do + if [ -d "$resource" ]; then + rm -rf "$resource" 2>/dev/null || true + elif [ -f "$resource" ]; then + rm -f "$resource" 2>/dev/null || true + fi + done + + # Clean up any additional temp directories + if [ -n "$TEMP_DIR" ] && [ -d "$TEMP_DIR" ]; then + rm -rf "$TEMP_DIR" 2>/dev/null || true + fi + + exit $exit_code +} + +# Set up trap for cleanup on script exit +setup_cleanup_trap() { + trap cleanup_resources EXIT INT TERM +} + +# Create a temporary directory with automatic cleanup +create_managed_temp_dir() { + local prefix="${1:-ai_thinking}" + local temp_dir + + temp_dir=$(mktemp -d -t "${prefix}_XXXXXX") + register_cleanup_resource "$temp_dir" + + echo "$temp_dir" +} + +# Create a temporary file with automatic cleanup +create_managed_temp_file() { + local prefix="${1:-ai_thinking}" + local suffix="${2:-tmp}" + local temp_file + + temp_file=$(mktemp -t "${prefix}_XXXXXX.${suffix}") + register_cleanup_resource "$temp_file" + + echo "$temp_file" +} + +# --- Standardized Error Handling --- + +# Standardized error codes +ERROR_INVALID_ARGUMENT=1 +ERROR_FILE_NOT_FOUND=2 +ERROR_MODEL_UNAVAILABLE=3 +ERROR_VALIDATION_FAILED=4 +ERROR_PROCESSING_FAILED=5 +ERROR_RESOURCE_ERROR=6 + +# Standardized error handling function +handle_error() { + local error_code="$1" + local error_message="$2" + local script_name="${3:-$(basename "${BASH_SOURCE[1]}")}" + local line_number="${4:-${BASH_LINENO[0]}}" + + # Log the error + log_error "[$script_name:$line_number] $error_message" + + # Print user-friendly error message + echo "Error: $error_message" >&2 + + # Exit with appropriate code + exit "$error_code" +} + +# Validation error handler +handle_validation_error() { + local error_message="$1" + local script_name="${2:-$(basename "${BASH_SOURCE[1]}")}" + + handle_error "$ERROR_VALIDATION_FAILED" "$error_message" "$script_name" "${BASH_LINENO[0]}" +} + +# Model error handler +handle_model_error() { + local model_name="$1" + local script_name="${2:-$(basename "${BASH_SOURCE[1]}")}" + + handle_error "$ERROR_MODEL_UNAVAILABLE" "Model '$model_name' is not available" "$script_name" "${BASH_LINENO[0]}" +} + +# File error handler +handle_file_error() { + local file_path="$1" + local operation="$2" + local script_name="${3:-$(basename "${BASH_SOURCE[1]}")}" + + handle_error "$ERROR_FILE_NOT_FOUND" "Cannot $operation file: $file_path" "$script_name" "${BASH_LINENO[0]}" +} + +# Processing error handler +handle_processing_error() { + local operation="$1" + local details="$2" + local script_name="${3:-$(basename "${BASH_SOURCE[1]}")}" + + handle_error "$ERROR_PROCESSING_FAILED" "Failed to $operation: $details" "$script_name" "${BASH_LINENO[0]}" +} + +# --- Utility Functions --- + +# Check if a command exists +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Create a temporary directory with cleanup +create_temp_dir() { + local prefix="${1:-ai_thinking}" + local temp_dir + + temp_dir=$(mktemp -d -t "${prefix}_XXXXXX") + echo "$temp_dir" +} + +# --- Common Constants --- + +# Default values +DEFAULT_ROUNDS=2 +DEFAULT_MODEL="gemma3n:e2b" + +# File paths +LOG_DIR=~/tmp/ai_thinking + +export DEFAULT_ROUNDS DEFAULT_MODEL LOG_DIR diff --git a/bash/talk-to-computer/computer b/bash/talk-to-computer/computer new file mode 100755 index 0000000..77fffcd --- /dev/null +++ b/bash/talk-to-computer/computer @@ -0,0 +1,477 @@ +#!/bin/bash + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Computer Dispatch System +# This script intelligently routes prompts to the most appropriate thinking mechanism +# or directly to Ollama based on complexity, question type, and user intent. +# +# APPLICATION LOGIC: +# The computer dispatch system implements an intelligent routing mechanism that +# analyzes user prompts and determines the optimal response strategy. The system +# operates through three distinct phases designed to maximize response quality: +# +# PHASE 1 - PROMPT ANALYSIS: +# - Analyzes prompt complexity, length, and question type +# - Identifies user intent and specific keywords +# - Determines if direct Ollama response is appropriate +# - Classifies prompts into response categories +# +# PHASE 2 - MECHANISM SELECTION: +# - Routes to appropriate thinking mechanism based on classification +# - Uses decision tree with keywords for clear cases +# - Considers prompt complexity and user intent +# - Falls back to direct Ollama for simple cases +# +# PHASE 3 - RESPONSE EXECUTION: +# - Executes the selected mechanism or direct Ollama call +# - Maintains transparency about the routing decision +# - Provides consistent output format regardless of mechanism +# - Logs the decision process for analysis +# +# DISPATCH MODELING: +# The system applies intelligent routing principles to AI response generation: +# - Prompt classification helps match complexity to appropriate mechanism +# - Keyword analysis identifies specific user needs and intent +# - Decision tree provides consistent, predictable routing logic +# - Direct Ollama routing handles simple cases efficiently +# - Transparency shows users how their prompt was processed +# - The system may improve response quality by using specialized mechanisms +# +# The dispatch process emphasizes efficiency and appropriateness, +# ensuring users get the best possible response for their specific needs. +# The system balances speed with depth based on prompt characteristics. + +# --- Model Configuration --- +DEFAULT_MODEL="gemma3n:e2b" + +# --- Defaults --- +DEFAULT_ROUNDS=2 + +# --- Argument Validation --- +if [ "$#" -lt 1 ]; then + show_computer_help + exit 1 +fi + +# Help function +show_computer_help() { + echo -e "\n\tComputer" + echo -e "\tThis script intelligently routes prompts to the most appropriate thinking mechanism" + echo -e "\tor directly to Ollama based on complexity, question type, and user intent." + echo -e "\n\tUsage: $0 [options] \"<your prompt>\" [number_of_rounds]" + echo -e "\n\tOptions:" + echo -e "\t -f <file_path> Append the contents of the file to the prompt" + echo -e "\t -d Force direct Ollama response (bypass thinking mechanisms)" + echo -e "\t -m <mechanism> Manually select thinking mechanism:" + echo -e "\t direct, socratic, exploration, consensus, critique," + echo -e "\t synthesis, peer-review, puzzle" + echo -e "\t -h, --help Show this help message" + echo -e "\n\tExamples:" + echo -e "\t $0 \"What is 2+2?\" # Auto-routing" + echo -e "\t $0 -f document.txt \"Analyze this\" 3 # With file, 3 rounds" + echo -e "\t $0 -d \"Simple question\" # Direct response only" + echo -e "\t $0 -m puzzle \"Using Lil, how can I...\" # Force puzzle mechanism" + echo -e "\n\tIf number_of_rounds is not provided, defaults to $DEFAULT_ROUNDS rounds." + echo -e "\n" +} + +# Available mechanisms +show_mechanisms() { + echo -e "\n\tAvailable Thinking Mechanisms:" + echo -e "\t direct - Simple questions, direct answers" + echo -e "\t socratic - Deep questioning and analysis" + echo -e "\t exploration - Multiple solution paths and comparison" + echo -e "\t consensus - Multiple model agreement" + echo -e "\t critique - Improvement suggestions and refinement" + echo -e "\t synthesis - Combining and integrating approaches" + echo -e "\t peer-review - Collaborative feedback and review" + echo -e "\t puzzle - Coding problems and Lil programming" + echo -e "\n" +} + +# --- Argument Parsing --- +FILE_PATH="" +FORCE_DIRECT=false +MANUAL_MECHANISM="" +while getopts "f:dm:h-:" opt; do + case $opt in + f) + FILE_PATH="$OPTARG" + ;; + d) + FORCE_DIRECT=true + ;; + m) + MANUAL_MECHANISM="$OPTARG" + ;; + h) + show_computer_help + exit 0 + ;; + -) + case "${OPTARG}" in + help) + show_computer_help + exit 0 + ;; + mechanisms) + show_mechanisms + exit 0 + ;; + *) + echo "Invalid option: --${OPTARG}" >&2 + exit 1 + ;; + esac + ;; + *) + echo "Invalid option: -$OPTARG" >&2 + show_computer_help + exit 1 + ;; + esac +done +shift $((OPTIND -1)) + +PROMPT="$1" +if [ -z "$2" ]; then + ROUNDS=$DEFAULT_ROUNDS +else + ROUNDS=$2 +fi + +# Store original prompt for validation after sourcing +ORIGINAL_PROMPT="$PROMPT" +ORIGINAL_FILE_PATH="$FILE_PATH" +ORIGINAL_ROUNDS="$ROUNDS" + +# Source the logging system using absolute path +source "${SCRIPT_DIR}/logging.sh" + +# Ensure validation functions are available +if ! command -v validate_prompt >/dev/null 2>&1; then + echo "Error: Validation functions not loaded properly" >&2 + exit 1 +fi + +# Validate and set default model with fallback +DEFAULT_MODEL=$(validate_model "$DEFAULT_MODEL" "llama3:8b-instruct-q4_K_M") +if [ $? -ne 0 ]; then + log_error "No valid default model available" + exit 1 +fi + +# Validate prompt +PROMPT=$(validate_prompt "$ORIGINAL_PROMPT") +if [ $? -ne 0 ]; then + exit 1 +fi + +# Validate file path if provided +if [ -n "$ORIGINAL_FILE_PATH" ]; then + if ! validate_file_path "$ORIGINAL_FILE_PATH"; then + exit 1 + fi + FILE_CONTENTS=$(cat "$ORIGINAL_FILE_PATH") + PROMPT="$PROMPT\n[FILE CONTENTS]\n$FILE_CONTENTS\n[END FILE]" +fi + +# Validate rounds +if ! [[ "$ORIGINAL_ROUNDS" =~ ^[1-9][0-9]*$ ]] || [ "$ORIGINAL_ROUNDS" -gt 5 ]; then + log_error "Invalid number of rounds: $ORIGINAL_ROUNDS (must be 1-5)" + exit 1 +fi + +# --- File Initialization --- +# Create a temporary directory if it doesn't exist +mkdir -p ~/tmp +# Create a unique file for this session based on the timestamp +SESSION_FILE=~/tmp/computer_$(date +%Y%m%d_%H%M%S).txt + +# Initialize timing +SESSION_ID=$(generate_session_id) +start_timer "$SESSION_ID" "computer" + +echo "Computer Dispatch Session Log: ${SESSION_FILE}" +echo "---------------------------------" + +# Store the initial user prompt in the session file +echo "USER PROMPT: ${PROMPT}" >> "${SESSION_FILE}" +echo "FORCE DIRECT: ${FORCE_DIRECT}" >> "${SESSION_FILE}" +echo "" >> "${SESSION_FILE}" + +# --- Advanced Prompt Analysis Function --- +analyze_prompt() { + local prompt="$1" + local use_advanced="${2:-true}" + + # Check for direct Ollama requests (explicit user intent) + if [[ "$prompt" =~ (direct|simple|quick|fast|straight) ]]; then + echo "DIRECT:1.0" + return + fi + + # Use advanced classification if available + if [ "$use_advanced" = "true" ] && [ -f "${SCRIPT_DIR}/classifier.sh" ]; then + source "${SCRIPT_DIR}/classifier.sh" + local result=$(classify_prompt "$prompt" true) + if [[ "$result" =~ ^[A-Z_]+:[0-9.]+$ ]]; then + echo "$result" + return + else + log_warning "Advanced classifier failed, falling back to simple classification" + fi + fi + + # Fallback to simple classification + local analysis="" + local confidence="0.6" + + # Check prompt length (simple heuristic for complexity) + local word_count=$(echo "$prompt" | wc -w) + + # Very short prompts (likely simple questions) + if [ "$word_count" -le 5 ]; then + echo "DIRECT:0.8" + return + fi + + # Keyword-based classification with priority order + if [[ "$prompt" =~ (consensus|agree|disagree|vote|multiple.*perspectives|multiple.*opinions) ]]; then + analysis="CONSENSUS" + elif [[ "$prompt" =~ (synthesize|combine|integrate|unify|merge|consolidate) ]]; then + analysis="SYNTHESIS" + elif [[ "$prompt" =~ (explore.*paths|explore.*alternatives|compare.*strategies|compare.*approaches|what.*options) ]]; then + analysis="EXPLORATION" + elif [[ "$prompt" =~ (improve|refine|edit|revise|better|enhance|polish|fix|optimize) ]]; then + analysis="CRITIQUE" + elif [[ "$prompt" =~ (review|feedback|peer.*review|collaborate|suggest|advice) ]]; then + analysis="PEER_REVIEW" + elif [[ "$prompt" =~ (analyze|examine|investigate|deep.*dive|thorough.*analysis|comprehensive) ]]; then + analysis="SOCRATIC" + elif [[ "$prompt" =~ (explore|alternatives|options|compare|strategies|approaches) ]]; then + analysis="EXPLORATION" + confidence="0.5" # Lower confidence due to ambiguous keywords + else + # Default to direct for unclear cases + analysis="DIRECT" + confidence="0.4" + fi + + echo "$analysis:$confidence" +} + +# --- Mechanism Selection --- +echo "Analyzing prompt and selecting mechanism..." +echo "PROMPT ANALYSIS:" >> "${SESSION_FILE}" + +if [ "$FORCE_DIRECT" = true ]; then + MECHANISM="DIRECT" + CONFIDENCE="1.0" + REASON="User requested direct response with -d flag" +else + # Check for manual mechanism selection + if [ -n "$MANUAL_MECHANISM" ]; then + # Validate manual mechanism selection + case "$MANUAL_MECHANISM" in + direct|DIRECT) + MECHANISM="DIRECT" + CONFIDENCE="1.0" + REASON="User manually selected direct mechanism" + ;; + socratic|SOCRATIC) + MECHANISM="SOCRATIC" + CONFIDENCE="1.0" + REASON="User manually selected socratic mechanism" + ;; + exploration|EXPLORATION) + MECHANISM="EXPLORATION" + CONFIDENCE="1.0" + REASON="User manually selected exploration mechanism" + ;; + consensus|CONSENSUS) + MECHANISM="CONSENSUS" + CONFIDENCE="1.0" + REASON="User manually selected consensus mechanism" + ;; + critique|CRITIQUE) + MECHANISM="CRITIQUE" + CONFIDENCE="1.0" + REASON="User manually selected critique mechanism" + ;; + synthesis|SYNTHESIS) + MECHANISM="SYNTHESIS" + CONFIDENCE="1.0" + REASON="User manually selected synthesis mechanism" + ;; + peer-review|peer_review|PEER_REVIEW|PEER-REVIEW) + MECHANISM="PEER_REVIEW" + CONFIDENCE="1.0" + REASON="User manually selected peer-review mechanism" + ;; + puzzle|PUZZLE) + MECHANISM="PUZZLE" + CONFIDENCE="1.0" + REASON="User manually selected puzzle mechanism" + ;; + *) + echo "Error: Invalid mechanism '$MANUAL_MECHANISM'" >&2 + echo "Use --mechanisms to see available options." >&2 + exit 1 + ;; + esac + else + ANALYSIS_RESULT=$(analyze_prompt "$PROMPT") + MECHANISM=$(echo "$ANALYSIS_RESULT" | cut -d':' -f1) + CONFIDENCE=$(echo "$ANALYSIS_RESULT" | cut -d':' -f2) + + # Validate confidence score + if [[ ! "$CONFIDENCE" =~ ^[0-9.]+$ ]]; then + CONFIDENCE="0.5" + log_warning "Invalid confidence score, defaulting to 0.5" + fi + fi + + case "$MECHANISM" in + "DIRECT") + REASON="Simple prompt or direct request (confidence: $CONFIDENCE)" + ;; + "CONSENSUS") + REASON="Multiple perspectives or consensus needed (confidence: $CONFIDENCE)" + ;; + "SYNTHESIS") + REASON="Integration of multiple approaches needed (confidence: $CONFIDENCE)" + ;; + "EXPLORATION") + REASON="Systematic exploration of alternatives needed (confidence: $CONFIDENCE)" + ;; + "SOCRATIC") + REASON="Deep analysis or exploration required (confidence: $CONFIDENCE)" + ;; + "CRITIQUE") + REASON="Improvement or refinement requested (confidence: $CONFIDENCE)" + ;; + "PEER_REVIEW") + REASON="Collaborative review or feedback needed (confidence: $CONFIDENCE)" + ;; + "PUZZLE") + REASON="Puzzle solving or coding challenge (confidence: $CONFIDENCE)" + ;; + *) + REASON="Default fallback (confidence: $CONFIDENCE)" + MECHANISM="DIRECT" + ;; + esac + + # Low confidence warning + if (( $(echo "$CONFIDENCE < 0.6" | bc -l 2>/dev/null || echo "0") )); then + log_warning "Low classification confidence ($CONFIDENCE) for prompt: $PROMPT" + echo "Note: Classification confidence is low ($CONFIDENCE). Consider using -d for direct response." >&2 + fi +fi + +echo "Selected mechanism: ${MECHANISM}" >> "${SESSION_FILE}" +echo "Reason: ${REASON}" >> "${SESSION_FILE}" +echo "" >> "${SESSION_FILE}" + +echo "Selected mechanism: ${MECHANISM}" +echo "Reason: ${REASON}" +echo "---------------------------------" + +# --- Response Execution --- +echo "Executing selected mechanism..." +echo "RESPONSE EXECUTION:" >> "${SESSION_FILE}" + +case "$MECHANISM" in + "DIRECT") + echo "Using direct Ollama response..." + echo "DIRECT OLLAMA RESPONSE:" >> "${SESSION_FILE}" + + DIRECT_PROMPT="You are an expert assistant. You always flag if you don't know something. Please provide a clear, helpful response to the following prompt: ${PROMPT}" + + RESPONSE=$(ollama run "${DEFAULT_MODEL}" "${DIRECT_PROMPT}") + + echo "${RESPONSE}" >> "${SESSION_FILE}" + echo "" >> "${SESSION_FILE}" + + echo "---------------------------------" + echo "Direct response:" + echo "---------------------------------" + echo "${RESPONSE}" + ;; + + "CONSENSUS") + echo "Delegating to consensus mechanism..." + echo "DELEGATING TO CONSENSUS:" >> "${SESSION_FILE}" + + # Execute consensus script and display output directly + "${SCRIPT_DIR}/consensus" "${PROMPT}" "${ROUNDS}" 2>&1 | tee -a "${SESSION_FILE}" + ;; + + "SOCRATIC") + echo "Delegating to Socratic mechanism..." + echo "DELEGATING TO SOCRATIC:" >> "${SESSION_FILE}" + + # Execute Socratic script and display output directly + "${SCRIPT_DIR}/socratic" "${PROMPT}" "${ROUNDS}" 2>&1 | tee -a "${SESSION_FILE}" + ;; + + "CRITIQUE") + echo "Delegating to critique mechanism..." + echo "DELEGATING TO CRITIQUE:" >> "${SESSION_FILE}" + + # Execute critique script and display output directly + "${SCRIPT_DIR}/critique" "${PROMPT}" "${ROUNDS}" 2>&1 | tee -a "${SESSION_FILE}" + ;; + + "PEER_REVIEW") + echo "Delegating to peer-review mechanism..." + echo "DELEGATING TO PEER_REVIEW:" >> "${SESSION_FILE}" + + # Execute peer-review script and display output directly + "${SCRIPT_DIR}/peer-review" "${PROMPT}" "${ROUNDS}" 2>&1 | tee -a "${SESSION_FILE}" + ;; + + "SYNTHESIS") + echo "Delegating to synthesis mechanism..." + echo "DELEGATING TO SYNTHESIS:" >> "${SESSION_FILE}" + + # Execute synthesis script and display output directly + "${SCRIPT_DIR}/synthesis" "${PROMPT}" "${ROUNDS}" 2>&1 | tee -a "${SESSION_FILE}" + ;; + + "EXPLORATION") + echo "Delegating to exploration mechanism..." + echo "DELEGATING TO EXPLORATION:" >> "${SESSION_FILE}" + + # Execute exploration script and display output directly + "${SCRIPT_DIR}/exploration" "${PROMPT}" "${ROUNDS}" 2>&1 | tee -a "${SESSION_FILE}" + ;; + + "PUZZLE") + echo "Delegating to puzzle mechanism..." + echo "DELEGATING TO PUZZLE:" >> "${SESSION_FILE}" + + # Execute puzzle script and display output directly + "${SCRIPT_DIR}/puzzle" "${PROMPT}" "${ROUNDS}" 2>&1 | tee -a "${SESSION_FILE}" + ;; +esac + +# --- Final Summary --- +echo "" >> "${SESSION_FILE}" +echo "DISPATCH SUMMARY:" >> "${SESSION_FILE}" +echo "================" >> "${SESSION_FILE}" +echo "Original Prompt: ${PROMPT}" >> "${SESSION_FILE}" +echo "Selected Mechanism: ${MECHANISM}" >> "${SESSION_FILE}" +echo "Reason: ${REASON}" >> "${SESSION_FILE}" +echo "Rounds: ${ROUNDS}" >> "${SESSION_FILE}" + +# End timing +duration=$(end_timer "$SESSION_ID" "computer") + +echo "" +echo "Execution time: ${duration} seconds" +echo "Full dispatch log: ${SESSION_FILE}" +echo "Full dispatch log: ${SESSION_FILE}" \ No newline at end of file diff --git a/bash/talk-to-computer/config.sh b/bash/talk-to-computer/config.sh new file mode 100755 index 0000000..ec612cc --- /dev/null +++ b/bash/talk-to-computer/config.sh @@ -0,0 +1,126 @@ +#!/bin/bash + +# Centralized Configuration File +# This file contains all model configurations, defaults, and system settings +# for the AI thinking mechanisms system. + +# --- Default Models --- + +# Main dispatcher model +DEFAULT_MODEL="gemma3n:e2b" + +# Classification model +CLASSIFIER_MODEL="gemma3n:e2b" + +# --- Thinking Mechanism Models --- + +# Exploration mechanism +EXPLORATION_MODEL="llama3:8b-instruct-q4_K_M" +ANALYSIS_MODEL="phi3:3.8b-mini-4k-instruct-q4_K_M" + +# Consensus mechanism +CONSENSUS_MODELS=( + "llama3:8b-instruct-q4_K_M" + "phi3:3.8b-mini-4k-instruct-q4_K_M" + "deepseek-r1:1.5b" + "gemma3n:e2b" + "dolphin3:latest" +) +CONSENSUS_JUDGE_MODEL="gemma3n:e2b" + +# Socratic mechanism +SOCRATIC_RESPONSE_MODEL="llama3:8b-instruct-q4_K_M" +SOCRATIC_QUESTION_MODEL="phi3:3.8b-mini-4k-instruct-q4_K_M" + +# Critique mechanism +CRITIQUE_MODEL="llama3:8b-instruct-q4_K_M" + +# Synthesis mechanism +SYNTHESIS_MODEL="llama3:8b-instruct-q4_K_M" + +# Peer Review mechanism +PEER_REVIEW_MODEL="llama3:8b-instruct-q4_K_M" + +# Puzzle mechanism +PUZZLE_MODEL="llama3:8b-instruct-q4_K_M" +PUZZLE_ANALYSIS_MODEL="phi3:3.8b-mini-4k-instruct-q4_K_M" + +# --- System Settings --- + +# Default values +DEFAULT_ROUNDS=2 +DEFAULT_LANGUAGE="lil" + +# Quality Guard settings +MIN_RESPONSE_LENGTH=30 +MAX_REPETITION_RATIO=0.4 +MAX_NONSENSE_SCORE=0.6 +DEGRADATION_THRESHOLD=0.65 +MAX_CORRECTION_ATTEMPTS=2 +FALLBACK_ENABLED=true + +# Logging settings +LOG_DIR=~/tmp/ai_thinking +SESSION_LOG="${LOG_DIR}/session_$(date +%Y%m%d_%H%M%S).json" +ERROR_LOG="${LOG_DIR}/errors.log" +METRICS_FILE="${LOG_DIR}/performance_metrics.json" +CLASSIFICATION_LOG="${LOG_DIR}/classification.log" + +# Security settings +MAX_PROMPT_LENGTH=10000 + +# --- Model Fallbacks --- + +# Fallback model for any model that fails validation +FALLBACK_MODEL="gemma3n:e2b" + +# --- Environment Variable Support --- + +# Allow overriding models via environment variables +if [ -n "$AI_DEFAULT_MODEL" ]; then + DEFAULT_MODEL="$AI_DEFAULT_MODEL" +fi + +if [ -n "$AI_CLASSIFIER_MODEL" ]; then + CLASSIFIER_MODEL="$AI_CLASSIFIER_MODEL" +fi + +if [ -n "$AI_EXPLORATION_MODEL" ]; then + EXPLORATION_MODEL="$AI_EXPLORATION_MODEL" +fi + +if [ -n "$AI_ANALYSIS_MODEL" ]; then + ANALYSIS_MODEL="$AI_ANALYSIS_MODEL" +fi + +if [ -n "$AI_PUZZLE_MODEL" ]; then + PUZZLE_MODEL="$AI_PUZZLE_MODEL" +fi + +# --- Utility Functions --- + +# Get a model with fallback support +get_model_with_fallback() { + local primary_model="$1" + local fallback_model="$2" + + if [ -n "$primary_model" ]; then + echo "$primary_model" + else + echo "$fallback_model" + fi +} + +# Validate if a model is available +is_model_available() { + local model="$1" + ollama list 2>/dev/null | grep -q "$model" +} + +# Export all configuration variables +export DEFAULT_MODEL CLASSIFIER_MODEL EXPLORATION_MODEL ANALYSIS_MODEL +export CONSENSUS_MODELS CONSENSUS_JUDGE_MODEL SOCRATIC_RESPONSE_MODEL SOCRATIC_QUESTION_MODEL +export CRITIQUE_MODEL SYNTHESIS_MODEL PEER_REVIEW_MODEL PUZZLE_MODEL PUZZLE_ANALYSIS_MODEL +export DEFAULT_ROUNDS DEFAULT_LANGUAGE MIN_RESPONSE_LENGTH MAX_REPETITION_RATIO +export MAX_NONSENSE_SCORE DEGRADATION_THRESHOLD MAX_CORRECTION_ATTEMPTS FALLBACK_ENABLED +export LOG_DIR SESSION_LOG ERROR_LOG METRICS_FILE CLASSIFICATION_LOG MAX_PROMPT_LENGTH FALLBACK_MODEL diff --git a/bash/consensus b/bash/talk-to-computer/consensus index f978614..4089dfa 100755 --- a/bash/consensus +++ b/bash/talk-to-computer/consensus @@ -45,6 +45,18 @@ # while random judge selection helps prevent single-model dominance. # The system emphasizes transparency and reliability in the decision process. +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source the logging system +source "${SCRIPT_DIR}/logging.sh" + +# Source the quality guard for output quality protection +source "${SCRIPT_DIR}/quality_guard.sh" + +# Get mechanism name automatically +MECHANISM_NAME=$(get_mechanism_name "$0") + # --- Model Configuration --- MODELS=( "llama3:8b-instruct-q4_K_M" @@ -167,6 +179,7 @@ high Make sure to include both [RESPONSE] and [CONFIDENCE] tags exactly as shown." response_output=$(ollama run "${model}" "${RESPONSE_PROMPT}") + response_output=$(guard_output_quality "$response_output" "$PROMPT" "$MECHANISM_NAME" "$model") # Extract response and confidence response_text=$(echo "${response_output}" | sed -n '/\[RESPONSE\]/,/\[CONFIDENCE\]/p' | sed '1d;$d' | sed '$d') @@ -224,6 +237,7 @@ CLAIMED CONFIDENCE: ${confidence} Based on the quality, completeness, and accuracy of this response, what is your confidence level? Respond with only: low, medium, or high" judge_output=$(ollama run "${JUDGE_MODEL}" "${JUDGE_PROMPT}") + judge_output=$(guard_output_quality "$judge_output" "$PROMPT" "$MECHANISM_NAME" "$JUDGE_MODEL") judge_confidence=$(echo "${judge_output}" | tr '[:upper:]' '[:lower:]' | grep -o -i "\(low\|medium\|high\)" | head -n1) # Validate judge confidence @@ -276,6 +290,7 @@ Please vote by responding with only the model number (1-${#MODELS[@]}) that you echo " Getting vote from ${model}..." vote_output=$(ollama run "${model}" "${voting_prompt}") + vote_output=$(guard_output_quality "$vote_output" "$PROMPT" "$MECHANISM_NAME" "$model") vote=$(echo "${vote_output}" | grep -o '[0-9]\+' | head -1) # Validate vote diff --git a/bash/talk-to-computer/corpus/.file_processors b/bash/talk-to-computer/corpus/.file_processors new file mode 100644 index 0000000..0c00161 --- /dev/null +++ b/bash/talk-to-computer/corpus/.file_processors @@ -0,0 +1,3 @@ +txt|cat +md|cat +html|cat diff --git a/bash/talk-to-computer/corpus/.topic_keywords b/bash/talk-to-computer/corpus/.topic_keywords new file mode 100644 index 0000000..486c24e --- /dev/null +++ b/bash/talk-to-computer/corpus/.topic_keywords @@ -0,0 +1,6 @@ +programming|bash shell scripting code algorithm programming software development +lil|decker lil language terse programming scripting deck +science|physics chemistry biology research scientific experiment +physics|quantum relativity mechanics thermodynamics energy force +literature|book author writing novel poem analysis criticism +general|knowledge fact information general misc miscellaneous diff --git a/bash/talk-to-computer/corpus/README.md b/bash/talk-to-computer/corpus/README.md new file mode 100644 index 0000000..d87af43 --- /dev/null +++ b/bash/talk-to-computer/corpus/README.md @@ -0,0 +1,236 @@ +# RAG Knowledge Corpus + +This directory contains the knowledge corpus for the RAG (Retrieval-Augmented Generation) system. The corpus is organized as a structured knowledge base that can be searched and used to augment AI responses with relevant context. + +## 📁 Directory Structure + +``` +corpus/ +├── README.md # This file +├── corpus_registry.txt # Auto-generated registry of available topics +├── corpus_manager.sh # Management script (in parent directory) +├── topic_template.md # Template for new topics +├── .topic_keywords # Topic keyword mappings +├── .file_processors # File processing configurations +│ +├── programming/ # Programming topics +│ ├── lil/ # Lil programming language +│ │ └── guide.md +│ └── algorithms.txt +│ +├── science/ # Scientific topics +│ ├── physics.txt +│ └── biology.md +│ +├── literature/ # Literary topics +├── general/ # General knowledge +└── examples/ # Example content +``` + +## 🔧 Management Tools + +### Corpus Manager (`./corpus_manager.sh`) + +The corpus manager provides utilities for managing the knowledge base: + +```bash +# Update the corpus registry (run after adding new files) +./corpus_manager.sh update + +# List all available topics +./corpus_manager.sh list + +# Check if a topic exists +./corpus_manager.sh exists programming + +# List files in a specific topic +./corpus_manager.sh files programming + +# Create template files for a new topic +./corpus_manager.sh template newtopic + +# Get corpus statistics +./corpus_manager.sh count science +``` + +### RAG Search (`./rag_search.sh`) + +Search the corpus using efficient Unix tools: + +```bash +# Search entire corpus +./rag_search.sh search "quantum physics" + +# Search specific topic +./rag_search.sh search "lil programming" programming + +# Get context around matches +./rag_search.sh context "variables" programming + +# Extract relevant sections +./rag_search.sh extract "functions" programming + +# Show corpus statistics +./rag_search.sh stats +``` + +## 📝 File Format Guidelines + +### Supported Formats +- **`.txt`** - Plain text files +- **`.md`** - Markdown files (recommended) +- **`.html`** - HTML files + +### Content Organization +1. **Use clear, descriptive headers** (`#`, `##`, `###`) +2. **Include examples and code blocks** where relevant +3. **Add cross-references** between related topics +4. **Use consistent formatting** and terminology +5. **Include practical applications** and use cases + +### Markdown Template +```markdown +# Topic Name - Comprehensive Guide + +## Introduction +[Brief overview of the topic] + +## Core Concepts +### [Subtopic 1] +[Explanation and details] + +### [Subtopic 2] +[Explanation and details] + +## Examples +[Code examples, diagrams, practical applications] + +## Best Practices +[Recommended approaches and common pitfalls] + +## References +[Links to additional resources] +``` + +## ➕ Adding New Content + +### Step 1: Create Topic Directory +```bash +# Create a new topic directory +mkdir -p corpus/newtopic + +# Or use the template command +./corpus_manager.sh template newtopic +``` + +### Step 2: Add Content Files +```bash +# Create content files in your preferred format +vim corpus/newtopic/guide.md +vim corpus/newtopic/examples.txt +vim corpus/newtopic/reference.html +``` + +### Step 3: Update Registry +```bash +# Update the corpus registry to include new files +./corpus_manager.sh update + +# Verify the topic is recognized +./corpus_manager.sh exists newtopic +./corpus_manager.sh files newtopic +``` + +### Step 4: Test Search +```bash +# Test that content is searchable +./rag_search.sh search "keyword" newtopic +./rag_search.sh context "concept" newtopic +``` + +## 🔍 Search Behavior + +### Keyword Matching +- **Case-insensitive** search across all text files +- **Multi-word queries** supported +- **Partial matches** found within words +- **Context extraction** shows surrounding lines + +### Topic Filtering +- **General search**: Searches entire corpus +- **Topic-specific**: Limited to specific directories +- **Hierarchical**: Supports subtopics (e.g., `science/physics`) + +### Performance +- **Sub-second lookups** using Unix tools +- **Efficient grep/sed/awk** processing +- **Cached registry** for fast topic discovery +- **Minimal memory usage** + +## 🔧 Advanced Configuration + +### Custom Topic Keywords +Edit `corpus/.topic_keywords` to add custom topic detection: +``` +newtopic|keyword1 keyword2 keyword3 +``` + +### File Processors +Edit `corpus/.file_processors` to add support for new file types: +``` +custom|processing_command +``` + +### Registry Customization +The `corpus_registry.txt` file can be manually edited for custom topic mappings: +``` +topic|path/to/files|keywords|description +``` + +## 🎯 Integration with AI Systems + +The corpus is designed to integrate with AI thinking mechanisms: + +### Automatic RAG Detection +- **Query analysis** determines when corpus search is needed +- **Topic classification** matches queries to appropriate corpus sections +- **Confidence scoring** determines RAG vs direct response + +### Context Injection +- **Relevant sections** extracted and formatted +- **Context length** managed to stay within token limits +- **Multiple sources** combined for comprehensive answers + +### Fallback Strategy +- **Graceful degradation** when no relevant corpus found +- **Direct LLM response** when corpus search yields no results +- **Error handling** for missing or corrupted files + +## 📊 Current Corpus Statistics + +*Run `./rag_search.sh stats` to see current corpus statistics.* + +## 🚀 Best Practices + +1. **Keep files focused** - One topic per file when possible +2. **Use descriptive names** - File names should indicate content +3. **Regular updates** - Run `update` after adding new files +4. **Test searches** - Verify content is discoverable +5. **Cross-reference** - Link related topics when appropriate +6. **Version control** - Track changes to corpus files + +## 🔄 Maintenance + +### Regular Tasks +- Run `./corpus_manager.sh update` after adding files +- Test search functionality with new content +- Review and update outdated information +- Archive unused or deprecated topics + +### Performance Monitoring +- Monitor search response times +- Check registry file size and complexity +- Validate file integrity periodically +- Clean up temporary search files + +This corpus system provides a scalable, efficient foundation for knowledge-augmented AI responses while maintaining the flexibility to grow and adapt to new requirements. diff --git a/bash/talk-to-computer/corpus/corpus_registry.txt b/bash/talk-to-computer/corpus/corpus_registry.txt new file mode 100644 index 0000000..2c1bae3 --- /dev/null +++ b/bash/talk-to-computer/corpus/corpus_registry.txt @@ -0,0 +1,9 @@ +# Corpus Registry - Auto-generated by corpus_manager.sh +# Format: TOPIC|PATH|KEYWORDS|DESCRIPTION +# This file is automatically maintained - do not edit manually + +examples|corpus/examples|examples|Examples topics and resources +general|corpus/general|general|General topics and resources +literature|corpus/literature|books,authors,literature,writing,analysis|Literature topics and resources +programming|corpus/programming|bash,shell,scripting,programming,lil,algorithm,code,software,development|Programming topics and resources +science|corpus/science|physics,chemistry,biology,science,research,scientific|Science topics and resources diff --git a/bash/talk-to-computer/corpus/corpus_registry.txt.backup b/bash/talk-to-computer/corpus/corpus_registry.txt.backup new file mode 100644 index 0000000..2c1bae3 --- /dev/null +++ b/bash/talk-to-computer/corpus/corpus_registry.txt.backup @@ -0,0 +1,9 @@ +# Corpus Registry - Auto-generated by corpus_manager.sh +# Format: TOPIC|PATH|KEYWORDS|DESCRIPTION +# This file is automatically maintained - do not edit manually + +examples|corpus/examples|examples|Examples topics and resources +general|corpus/general|general|General topics and resources +literature|corpus/literature|books,authors,literature,writing,analysis|Literature topics and resources +programming|corpus/programming|bash,shell,scripting,programming,lil,algorithm,code,software,development|Programming topics and resources +science|corpus/science|physics,chemistry,biology,science,research,scientific|Science topics and resources diff --git a/bash/talk-to-computer/corpus/programming/combinators.md b/bash/talk-to-computer/corpus/programming/combinators.md new file mode 100644 index 0000000..8e2cfb0 --- /dev/null +++ b/bash/talk-to-computer/corpus/programming/combinators.md @@ -0,0 +1,192 @@ +# Combinators - The Ultimate Reusable Functions + +## Introduction + +In the context of functional programming and computer science, a **combinator** is a higher-order function that uses only function application and other combinators to define a result. Crucially, a combinator contains **no free variables**. This means it is a completely self-contained function that only refers to its own arguments. + +Combinators are fundamental concepts from **combinatory logic** and **lambda calculus**. While they have deep theoretical importance, their practical application in software development is to create highly reusable, abstract, and composable code, often leading to a **point-free** or **tacit** programming style. They are the essential glue for building complex logic by piecing together simpler functions. + +## Core Concepts + +### No Free Variables + +The defining characteristic of a combinator is that it has no **free variables**. A free variable is a variable referenced in a function that is not one of its formal arguments or defined within the function's local scope. This self-contained nature makes combinators perfectly portable and predictable. + +```javascript +const y = 10; + +// This function is NOT a combinator because it uses a free variable `y`. +// Its behavior depends on an external context. +const addY = (x) => x + y; + +// This function IS a combinator. It has no free variables. +// Its behavior only depends on its arguments. +const add = (x) => (z) => x + z; +``` + +### Function Composition and Transformation + +Combinators are designed to manipulate and combine other functions. They are the building blocks for creating new functions from existing ones without needing to specify the data that the functions will eventually operate on. The entire logic is expressed as a transformation of functions themselves. + +## Key Principles + + - **Point-Free Style (Tacit Programming)**: This is the primary programming style associated with combinators. You define functions as a pipeline or composition of other functions without explicitly mentioning the arguments (the "points"). This can lead to more abstract and declarative code. + + ```javascript + // Not point-free: the argument `users` is explicitly mentioned. + const getActiveUserNames = (users) => users.filter(user => user.active).map(user => user.name); + + // Point-free style: built by composing functions. + // `compose`, `filter`, `map`, and `prop` are all combinators or higher-order functions. + const getActiveUserNamesPointFree = compose(map(prop('name')), filter(propEq('active', true))); + ``` + + - **Abstraction**: Combinators abstract common patterns of execution and control flow. For example, the act of applying one function's result to another is abstracted away by the `compose` combinator. + +## Implementation/Usage + +Many famous combinators have single-letter names from combinatory logic. Understanding them helps in recognizing fundamental patterns. + +### Basic Example + +The simplest combinators are the **I-combinator (Identity)** and the **K-combinator (Constant)**. + +```javascript +/** + * I-combinator (Identity) + * Takes a value and returns it. + * I x = x + */ +const I = (x) => x; + +/** + * K-combinator (Constant or Kestrel) + * Takes two arguments and returns the first. Creates constant functions. + * K x y = x + */ +const K = (x) => (y) => x; + +// Usage: +const value = I("hello"); // "hello" +const always42 = K(42); +const result = always42("some other value"); // 42 +``` + +### Advanced Example + +More complex combinators handle function composition, like the **B-combinator (Bluebird)**. + +```javascript +/** + * B-combinator (Bluebird / Function Composition) + * Composes two functions. + * B f g x = f (g x) + */ +const B = (f) => (g) => (x) => f(g(x)); + +// In practice, this is often implemented as `compose`. +const compose = (f, g) => (x) => f(g(x)); + +// Usage: +const double = (n) => n * 2; +const increment = (n) => n + 1; + +// Create a new function that increments then doubles. +const incrementThenDouble = compose(double, increment); + +incrementThenDouble(5); // Returns 12, because (5 + 1) * 2 +``` + +Another useful combinator is the **T-combinator (Thrush)**, which applies a value to a function. + +```javascript +/** + * T-combinator (Thrush) + * Takes a value and a function, and applies the function to the value. + * T x f = f x + */ +const T = (x) => (f) => f(x); + +// This is the basis for the `pipe` or "thread-first" operator. +T(5, increment); // 6 +``` + +## Common Patterns + +### Pattern 1: Function Composition (`compose` / `pipe`) + +This is the most common and practical application of combinators. `compose` (based on the B-combinator) applies functions from right to left, while `pipe` applies them from left to right. They are used to build data-processing pipelines in a point-free style. + +```javascript +// Ramda-style compose, handles multiple functions +const compose = (...fns) => (initialVal) => fns.reduceRight((val, fn) => fn(val), initialVal); +const pipe = (...fns) => (initialVal) => fns.reduce((val, fn) => fn(val), initialVal); +``` + +### Pattern 2: Parser Combinators + +A parser combinator is a higher-order function that takes several parsers as input and returns a new parser as its output. This is an advanced technique for building complex parsers by combining simple, specialized parsers for different parts of a grammar. It's a powerful real-world application of combinator logic. + +## Best Practices + + - **Prioritize Readability**: While point-free style can be elegant, it can also become cryptic. If a composition is too long or complex, break it down and give intermediate functions meaningful names. + - **Know Your Library**: If you are using a functional programming library like Ramda or fp-ts, invest time in learning the combinators it provides. They are the building blocks for effective use of the library. + - **Use Currying**: Combinators are most powerful in a language that supports currying, as it allows for partial application, creating specialized functions from general ones. + +## Common Pitfalls + + - **"Pointless" Code**: Overuse of point-free style can lead to code that is very difficult to read and debug. The goal is clarity through abstraction, not just character count reduction. + - **Debugging Complexity**: Debugging a long chain of composed functions is challenging because there are no named intermediate values to inspect. You often have to break the chain apart to find the source of a bug. + +## Performance Considerations + + - **Function Call Overhead**: In theory, a deeply nested composition of combinators can introduce a small overhead from the additional function calls. + - **Negligible in Practice**: In most real-world applications, this overhead is negligible and completely optimized away by modern JavaScript engines and language compilers. Code clarity and correctness are far more important concerns. + +## Integration Points + + - **Functional Programming Libraries**: Libraries like **Ramda**, **Lodash/fp**, and the **Haskell Prelude** are essentially collections of combinators and other higher-order functions. + - **Lambda Calculus**: Combinatory logic, the formal study of combinators, is computationally equivalent to lambda calculus. The famous **SKI combinator calculus** (using only S, K, and I combinators) can be used to express any computable algorithm. + - **Parser Combinator Libraries**: Libraries like `parsec` in Haskell or `fast-check` in JavaScript use these principles to build robust parsers and property-based testing tools. + +## Troubleshooting + +### Problem 1: A Composed Function Behaves Incorrectly + +**Symptoms:** The final output of a point-free pipeline is `undefined`, `NaN`, or simply the wrong value. +**Solution:** Temporarily "re-point" the function to debug. Break the composition and insert `console.log` statements (or a `tap` utility function) to inspect the data as it flows from one function to the next. + +```javascript +// A "tap" combinator is useful for debugging. +const tap = (fn) => (x) => { + fn(x); + return x; +}; + +// Insert it into a pipeline to inspect intermediate values. +const problematicPipe = pipe( + increment, + tap(console.log), // See the value after incrementing + double +); +``` + +## Examples in Context + + - **Configuration Objects**: Using the K-combinator (constant function) to provide default configuration values. + - **Data Validation**: Building a validator by composing smaller validation rule functions, where each function takes data and returns either a success or failure indicator. + - **Web Development**: A point-free pipeline in a frontend application that takes a raw API response, filters out inactive items, extracts a specific field, and formats it for display. + +## References + + - [To Mock a Mockingbird by Raymond Smullyan](https://en.wikipedia.org/wiki/To_Mock_a_Mockingbird) - An accessible and famous book that teaches combinatory logic through recreational puzzles. + - [Wikipedia: Combinatory Logic](https://en.wikipedia.org/wiki/Combinatory_logic) + - [Ramda Documentation](https://ramdajs.com/docs/) + +## Related Topics + + - Point-Free Style + - Lambda Calculus + - Functional Programming + - Currying + - Higher-Order Functions \ No newline at end of file diff --git a/bash/talk-to-computer/corpus/programming/command_line_data_processing.md b/bash/talk-to-computer/corpus/programming/command_line_data_processing.md new file mode 100644 index 0000000..c5ce5f5 --- /dev/null +++ b/bash/talk-to-computer/corpus/programming/command_line_data_processing.md @@ -0,0 +1,200 @@ +# Local Data Processing With Unix Tools - Shell-based data wrangling + +## Introduction + +Leveraging standard Unix command-line tools for data processing is a powerful, efficient, and universally available method for handling text-based data. This guide focuses on the **Unix philosophy** of building complex data processing **pipelines** by composing small, single-purpose utilities. This approach is invaluable for ad-hoc data exploration, log analysis, and pre-processing tasks directly within the shell, often outperforming more complex scripts or dedicated software for common data wrangling operations. + +Key applications include analyzing web server logs, filtering and transforming CSV/TSV files, and batch-processing any line-oriented text data. + +## Core Concepts + +### Streams and Redirection + +At the core of Unix inter-process communication are three standard streams: + +1. `stdin` (standard input): The stream of data going into a program. +2. `stdout` (standard output): The primary stream of data coming out of a program. +3. `stderr` (standard error): A secondary output stream for error messages and diagnostics. + +**Redirection** controls these streams. The pipe `|` operator is the most important, as it connects one command's `stdout` to the next command's `stdin`, forming a pipeline. + +```bash +# Redirect stdout to a file (overwrite) +command > output.txt + +# Redirect stdout to a file (append) +command >> output.txt + +# Redirect a file to stdin +command < input.txt + +# Redirect stderr to a file +command 2> error.log + +# Redirect stderr to stdout +command 2>&1 +``` + +### The Core Toolkit + +A small set of highly-specialized tools forms the foundation of most data pipelines. + + - **`grep`**: Filters lines that match a regular expression. + - **`awk`**: A powerful pattern-scanning and processing language. It excels at columnar data, allowing you to manipulate fields within each line. + - **`sed`**: A "stream editor" for performing text transformations on an input stream (e.g., search and replace). + - **`sort`**: Sorts lines of text files. + - **`uniq`**: Reports or omits repeated lines. Often used with `-c` to count occurrences. + - **`cut`**: Removes sections from each line of files (e.g., select specific columns). + - **`tr`**: Translates or deletes characters. + - **`xargs`**: Builds and executes command lines from standard input. It bridges the gap between commands that produce lists of files and commands that operate on them. + +## Key Principles + +The effectiveness of this approach stems from the **Unix Philosophy**: + +1. **Do one thing and do it well**: Each tool is specialized for a single task (e.g., `grep` only filters, `sort` only sorts). +2. **Write programs that work together**: The universal text stream interface (`stdin`/`stdout`) allows for near-infinite combinations of tools. +3. **Handle text streams**: Text is a universal interface, making the tools broadly applicable to a vast range of data formats. + +## Implementation/Usage + +Let's assume we have a web server access log file, `access.log`, with the following format: +`IP_ADDRESS - - [TIMESTAMP] "METHOD /path HTTP/1.1" STATUS_CODE RESPONSE_SIZE` + +Example line: +`192.168.1.10 - - [20/Aug/2025:15:30:00 -0400] "GET /home HTTP/1.1" 200 5120` + +### Basic Example + +**Goal**: Find the top 5 IP addresses that accessed the server. + +```bash +# This pipeline extracts, groups, counts, and sorts the IP addresses. +cat access.log | \ + awk '{print $1}' | \ + sort | \ + uniq -c | \ + sort -nr | \ + head -n 5 +``` + +**Breakdown:** + +1. `cat access.log`: Reads the file and sends its content to `stdout`. +2. `awk '{print $1}'`: For each line, print the first field (the IP address). +3. `sort`: Sorts the IPs alphabetically, which is necessary for `uniq` to group them. +4. `uniq -c`: Collapses adjacent identical lines into one and prepends the count. +5. `sort -nr`: Sorts the result numerically (`-n`) and in reverse (`-r`) order to get the highest counts first. +6. `head -n 5`: Takes the first 5 lines of the sorted output. + +### Advanced Example + +**Goal**: Calculate the total bytes served for all successful (`2xx` status code) `POST` requests. + +```bash +# This pipeline filters for specific requests and sums a column. +grep '"POST ' access.log | \ + grep ' 2[0-9][0-9] ' | \ + awk '{total += $10} END {print total}' +``` + +**Breakdown:** + +1. `grep '"POST ' access.log`: Filters the log for lines containing ` "POST ` (note the space to avoid matching other methods). +2. `grep ' 2[0-9][0-9] '`: Filters the remaining lines for a 2xx status code. The spaces ensure we match the status code field specifically. +3. `awk '{total += $10} END {print total}'`: For each line that passes the filters, `awk` adds the value of the 10th field (response size) to a running `total`. The `END` block executes after all lines are processed, printing the final sum. + +## Common Patterns + +### Pattern 1: Filter-Map-Reduce + +This is a functional programming pattern that maps directly to Unix pipelines. + + - **Filter**: Select a subset of data (`grep`, `head`, `tail`, `awk '/pattern/'`). + - **Map**: Transform each line of data (`awk '{...}'`, `sed 's/.../.../'`, `cut`). + - **Reduce**: Aggregate data into a summary result (`sort | uniq -c`, `wc -l`, `awk '{sum+=$1} END {print sum}'`). + +### Pattern 2: Shuffling (Sort-Based Grouping) + +This is the command-line equivalent of a `GROUP BY` operation in SQL. The pattern is to extract a key, sort by that key to group related records together, and then process each group. + +```bash +# Example: Find the most frequent user agent for each IP address. +# The key here is the IP address ($1). +awk '{print $1, $12}' access.log | \ + sort | \ + uniq -c | \ + sort -k2,2 -k1,1nr | \ + awk 'BEGIN{last=""} {if ($2 != last) {print} last=$2}' +``` + +This advanced pipeline sorts by IP, then by count, and finally uses `awk` to pick the first (highest count) entry for each unique IP. + +## Best Practices + + - **Develop Incrementally**: Build pipelines one command at a time. After adding a `|` and a new command, run it to see if the intermediate output is what you expect. + - **Filter Early**: Place `grep` or other filtering commands as early as possible in the pipeline. This reduces the amount of data that subsequent, potentially more expensive commands like `sort` have to process. + - **Use `set -o pipefail`**: In shell scripts, this option causes a pipeline to return a failure status if *any* command in the pipeline fails, not just the last one. + - **Prefer `awk` for Columns**: For tasks involving multiple columns, `awk` is generally more powerful, readable, and performant than a complex chain of `cut`, `paste`, and shell loops. + - **Beware of Locales**: The `sort` command's behavior is affected by the `LC_ALL` environment variable. For byte-wise sorting, use `LC_ALL=C sort`. + +## Common Pitfalls + + - **Forgetting to Sort Before `uniq`**: `uniq` only operates on adjacent lines. If the data is not sorted, it will not produce correct counts. + - **Greedy Regular Expressions**: A `grep` pattern like ` . ` can match more than intended. Be as specific as possible with your regex. + - **Shell Globbing vs. `grep` Regex**: The wildcards used by the shell (`*`, `?`) are different from those used in regular expressions (`.*`, `.`). + - **Word Splitting on Unquoted Variables**: When used in scripts, variables containing spaces can be split into multiple arguments if not quoted (`"my var"` vs `my var`). + +## Performance Considerations + + - **I/O is King**: These tools are often I/O-bound. Reading from and writing to disk is the slowest part. Use pipelines to avoid creating intermediate files. + - **`awk` vs. `sed` vs. `grep`**: For simple filtering, `grep` is fastest. For simple substitutions, `sed` is fastest. For any field-based logic, `awk` is the right tool and is extremely fast, as it's a single compiled process. + - **GNU Parallel**: For tasks that can be broken into independent chunks (e.g., processing thousands of files), `GNU parallel` can be used to execute pipelines in parallel, dramatically speeding up the work on multi-core systems. + +## Integration Points + + - **Shell Scripting**: These tools are the fundamental building blocks for automation and data processing scripts in `bash`, `zsh`, etc. + - **Data Ingestion Pipelines**: Unix tools are often used as the first step (the "T" in an ELT process) to clean, filter, and normalize raw log files before they are loaded into a database or data warehouse. + - **Other Languages**: Languages like Python (`subprocess`) and Go (`os/exec`) can invoke these command-line tools to leverage their performance and functionality without having to re-implement them. + +## Troubleshooting + +### Problem 1: Pipeline hangs or is extremely slow + +**Symptoms:** The command prompt doesn't return, and there's no output. +**Solution:** This is often caused by a command like `sort` or another tool that needs to read all of its input before producing any output. It may be processing a massive amount of data. + +1. Test your pipeline on a small subset of the data first using `head -n 1000`. +2. Use a tool like `pv` (pipe viewer) in the middle of your pipeline (`... | pv | ...`) to monitor the flow of data and see where it's getting stuck. + +### Problem 2: `xargs` fails on filenames with spaces + +**Symptoms:** An `xargs` command fails with "file not found" errors for files with spaces or special characters in their names. +**Solution:** Use the "null-delimited" mode of `find` and `xargs`, which is designed to handle all possible characters in filenames safely. + +```bash +# Wrong way, will fail on "file name with spaces.txt" +find . -name "*.txt" | xargs rm + +# Correct, safe way +find . -name "*.txt" -print0 | xargs -0 rm +``` + +## Examples in Context + + - **DevOps/SRE**: Quickly grepping through gigabytes of Kubernetes logs to find error messages related to a specific request ID. + - **Bioinformatics**: Processing massive FASTA/FASTQ text files to filter, reformat, or extract sequence data. + - **Security Analysis**: Analyzing `auth.log` files to find failed login attempts, group them by IP, and identify brute-force attacks. + +## References + + - [The GNU Coreutils Manual](https://www.gnu.org/software/coreutils/manual/coreutils.html) + - [The AWK Programming Language (Book by Aho, Kernighan, Weinberger)](https://archive.org/details/pdfy-MgN0H1joIoDVoIC7) + - [Greg's Wiki - Bash Pitfalls](https://mywiki.wooledge.org/BashPitfalls) + +## Related Topics + + - Shell Scripting + - Regular Expressions (Regex) + - AWK Programming + - Data Wrangling \ No newline at end of file diff --git a/bash/talk-to-computer/corpus/programming/functional_programming.md b/bash/talk-to-computer/corpus/programming/functional_programming.md new file mode 100644 index 0000000..2572442 --- /dev/null +++ b/bash/talk-to-computer/corpus/programming/functional_programming.md @@ -0,0 +1,234 @@ +# Functional Programming - A paradigm for declarative, predictable code + +## Introduction + +**Functional Programming (FP)** is a programming paradigm where software is built by composing **pure functions**, avoiding shared state, mutable data, and side-effects. It treats computation as the evaluation of mathematical functions. Instead of describing *how* to achieve a result (imperative programming), you describe *what* the result is (declarative programming). + +This paradigm has gained significant traction because it helps manage the complexity of modern applications, especially those involving concurrency and complex state management. Programs written in a functional style are often easier to reason about, test, and debug. + +## Core Concepts + +### Pure Functions + +A function is **pure** if it adheres to two rules: + +1. **The same input always returns the same output.** The function's return value depends solely on its input arguments. +2. **It produces no side effects.** A side effect is any interaction with the "outside world" from within the function. This includes modifying a global variable, changing an argument, logging to the console, or making a network request. + +<!-- end list --> + +```javascript +// Pure function: predictable and testable +const add = (a, b) => a + b; +add(2, 3); // Always returns 5 + +// Impure function: has a side effect (console.log) +let count = 0; +const incrementWithLog = () => { + count++; // And mutates external state + console.log(`The count is ${count}`); + return count; +}; +``` + +### Immutability + +**Immutability** means that data, once created, cannot be changed. If you need to modify a data structure (like an object or array), you create a new one with the updated values instead of altering the original. This prevents bugs caused by different parts of your application unexpectedly changing the same piece of data. + +```javascript +// Bad: Mutating an object +const user = { name: "Alice", age: 30 }; +const celebrateBirthdayMutable = (person) => { + person.age++; // This modifies the original user object + return person; +}; + +// Good: Returning a new object +const celebrateBirthdayImmutable = (person) => { + return { ...person, age: person.age + 1 }; // Creates a new object +}; + +const newUser = celebrateBirthdayImmutable(user); +// user is still { name: "Alice", age: 30 } +// newUser is { name: "Alice", age: 31 } +``` + +### First-Class and Higher-Order Functions + +In FP, functions are **first-class citizens**. This means they can be treated like any other value: + + * Assigned to variables + * Stored in data structures + * Passed as arguments to other functions + * Returned as values from other functions + +A function that either takes another function as an argument or returns a function is called a **Higher-Order Function**. Common examples are `map`, `filter`, and `reduce`. + +```javascript +const numbers = [1, 2, 3, 4]; +const isEven = (n) => n % 2 === 0; +const double = (n) => n * 2; + +// `filter` and `map` are Higher-Order Functions +const evenDoubled = numbers.filter(isEven).map(double); // [4, 8] +``` + +## Key Principles + + - **Declarative Style**: Focus on *what* the program should accomplish, not *how* it should accomplish it. An SQL query is a great example of a declarative style. + - **No Side Effects**: Isolate side effects from the core logic of your application. This makes your code more predictable. + - **Function Composition**: Build complex functionality by combining small, reusable functions. + - **Referential Transparency**: An expression can be replaced with its value without changing the behavior of the program. This is a natural outcome of using pure functions and immutable data. + +## Implementation/Usage + +The core idea is to create data transformation pipelines. You start with initial data and pass it through a series of functions to produce the final result. + +### Basic Example + +```javascript +// A simple pipeline for processing a list of users +const users = [ + { name: "Alice", active: true, score: 90 }, + { name: "Bob", active: false, score: 80 }, + { name: "Charlie", active: true, score: 95 }, +]; + +/** + * @param {object[]} users + * @returns {string[]} + */ +const getHighScoringActiveUserNames = (users) => { + return users + .filter((user) => user.active) + .filter((user) => user.score > 85) + .map((user) => user.name.toUpperCase()); +}; + +console.log(getHighScoringActiveUserNames(users)); // ["ALICE", "CHARLIE"] +``` + +### Advanced Example + +A common advanced pattern is to use a reducer function to manage application state, a core concept in The Elm Architecture and libraries like Redux. + +```javascript +// The state of our simple counter application +const initialState = { count: 0 }; + +// A pure function that describes how state changes in response to an action +const counterReducer = (state, action) => { + switch (action.type) { + case 'INCREMENT': + return { ...state, count: state.count + 1 }; + case 'DECREMENT': + return { ...state, count: state.count - 1 }; + case 'RESET': + return { ...state, count: 0 }; + default: + return state; + } +}; + +// Simulate dispatching actions +let state = initialState; +state = counterReducer(state, { type: 'INCREMENT' }); // { count: 1 } +state = counterReducer(state, { type: 'INCREMENT' }); // { count: 2 } +state = counterReducer(state, { type: 'DECREMENT' }); // { count: 1 } + +console.log(state); // { count: 1 } +``` + +## Common Patterns + +### Pattern 1: Functor + +A **Functor** is a design pattern for a data structure that can be "mapped over." It's a container that holds a value and has a `map` method for applying a function to that value without changing the container's structure. The most common example is the `Array`. + +```javascript +// Array is a Functor because it has a .map() method +const numbers = [1, 2, 3]; +const addOne = (n) => n + 1; +const result = numbers.map(addOne); // [2, 3, 4] +``` + +### Pattern 2: Monad + +A **Monad** is a pattern for sequencing computations. Think of it as a "safer" functor that knows how to handle nested contexts or operations that can fail (like Promises or the `Maybe` type). `Promise` is a good practical example; its `.then()` method (or `flatMap`) lets you chain asynchronous operations together seamlessly. + +```javascript +// Promise is a Monad, allowing chaining of async operations +const fetchUser = (id) => Promise.resolve({ id, name: "Alice" }); +const fetchUserPosts = (user) => Promise.resolve([ { userId: user.id, title: "Post 1" } ]); + +fetchUser(1) + .then(fetchUserPosts) // .then acts like flatMap here + .then(posts => console.log(posts)) + .catch(err => console.error(err)); +``` + +## Best Practices + + - **Keep Functions Small**: Each function should do one thing well. + - **Use Function Composition**: Use utilities like `pipe` or `compose` to build complex logic from simple building blocks. + - **Embrace Immutability**: Use `const` by default. Avoid reassigning variables. When updating objects or arrays, create new ones. + - **Isolate Impurity**: Side effects are necessary. Keep them at the boundaries of your application (e.g., in the function that handles an API call) and keep your core business logic pure. + +## Common Pitfalls + + - **Accidental Mutation**: JavaScript objects and arrays are passed by reference, making it easy to mutate them accidentally. Be vigilant, especially with nested data. + - **Over-Abstraction**: Don't use complex FP concepts like monad transformers if a simple function will do. Prioritize readability. + - **Performance Misconceptions**: While creating many short-lived objects can have a performance cost, modern JavaScript engines are highly optimized for this pattern. Don't prematurely optimize; measure first. + +## Performance Considerations + + - **Object/Array Creation**: In performance-critical code (e.g., animations, large data processing), the overhead of creating new objects/arrays in a tight loop can be significant. + - **Structural Sharing**: Libraries like `Immer` and `Immutable.js` use a technique called structural sharing. When you "change" an immutable data structure, only the parts that changed are created anew; the rest of the structure points to the same old data, saving memory and CPU time. + - **Recursion**: Deep recursion can lead to stack overflow errors. While some languages support **Tail Call Optimization (TCO)** to prevent this, JavaScript engines have limited support. Prefer iteration for very large data sets. + +## Integration Points + + - **UI Frameworks**: FP concepts are central to modern UI libraries. **React** encourages pure components and uses immutable state patterns with Hooks (`useState`, `useReducer`). + \-- **State Management**: Libraries like **Redux** and **Zustand** are built entirely on FP principles, particularly the use of pure reducer functions. + - **Data Processing**: FP is excellent for data transformation pipelines. It's often used in backend services for processing streams of data. + - **Utility Libraries**: Libraries like **Lodash/fp** and **Ramda** provide a rich toolkit of pre-built, curried, and pure functions for everyday tasks. + +## Troubleshooting + +### Problem 1: Debugging composed function pipelines + +**Symptoms:** A chain of `.map().filter().reduce()` produces an incorrect result, and it's hard to see where it went wrong. +**Solution:** Break the chain apart. Log the intermediate result after each step to inspect the data as it flows through the pipeline. + +```javascript +const result = users + .filter((user) => user.active) + // console.log('After active filter:', resultFromActiveFilter) + .filter((user) => user.score > 85) + // console.log('After score filter:', resultFromScoreFilter) + .map((user) => user.name.toUpperCase()); +``` + +### Problem 2: State changes unexpectedly + +**Symptoms:** A piece of state (e.g., in a React component or Redux store) changes when it shouldn't have, leading to bugs or infinite re-renders. +**Solution:** This is almost always due to accidental mutation. Audit your code to ensure you are not modifying state directly. Use the spread syntax (`...`) for objects and arrays (`[...arr, newItem]`) to create copies. Libraries like `Immer` can make this process safer and more concise. + +## Examples in Context + + - **Frontend Web Development**: The **Elm Architecture** (Model, Update, View) is a purely functional pattern for building web apps. It has heavily influenced libraries like Redux. + - **Data Analysis**: Running a series of transformations on a large dataset to filter, shape, and aggregate it for a report. + - **Concurrency**: Handling multiple events or requests simultaneously without running into race conditions, because data is immutable and shared state is avoided. + +## References + + - [MDN Web Docs: Functional Programming](https://www.google.com/search?q=https://developer.mozilla.org/en-US/docs/Glossary/Functional_programming) + - [Professor Frisby's Mostly Adequate Guide to Functional Programming](https://mostly-adequate.gitbook.io/mostly-adequate-guide/) + - [Ramda Documentation](https://ramdajs.com/docs/) + +## Related Topics + + - Immutability + - Functional Reactive Programming (FRP) + - The Elm Architecture + - Algebraic Data Types \ No newline at end of file diff --git a/bash/talk-to-computer/corpus/programming/lil_guide.md b/bash/talk-to-computer/corpus/programming/lil_guide.md new file mode 100644 index 0000000..72df8df --- /dev/null +++ b/bash/talk-to-computer/corpus/programming/lil_guide.md @@ -0,0 +1,277 @@ +# Multi-paradigm Programming with Lil - A Guide to Lil's Diverse Styles + +## Introduction + +Lil is a richly multi-paradigm scripting language designed for the Decker creative environment. It seamlessly blends concepts from **imperative**, **functional**, **declarative**, and **vector-oriented** programming languages. This flexibility allows developers to choose the most effective and ergonomic approach for a given task, whether it's managing application state, manipulating complex data structures, or performing efficient bulk computations. Understanding these paradigms is key to writing elegant, efficient, and idiomatic Lil code. + +## Core Concepts + +Lil's power comes from the way it integrates four distinct programming styles. + +### Imperative Programming + +This is the traditional, statement-by-statement style of programming. It involves creating variables, assigning values to them, and using loops and conditionals to control the flow of execution. + + - **Assignment:** The colon (`:`) is used for assignment. + - **Control Flow:** Lil provides `if`/`elseif`/`else` for conditionals and `while` and `each` for loops. + - **State Management:** State is typically managed by assigning and re-assigning values to variables, often stored in the properties of Decker widgets between event handlers. + +<!-- end list --> + +```lil +# Imperative approach to summing a list +total: 0 +numbers: [10, 20, 30] +each n in numbers do + total: total + n +end +# total is now 60 +``` + +### Functional Programming + +The functional style emphasizes pure functions, immutability, and the composition of functions without side-effects. + + - **Immutability:** All core data structures (lists, dictionaries, tables) have copy-on-write semantics. Modifying one does not alter the original value but instead returns a new, amended value. + - **First-Class Functions:** Functions are values that can be defined with `on`, assigned to variables, and passed as arguments to other functions. + - **Expressions over Statements:** Every statement in Lil is an expression that returns a value. An `if` block returns the value of its executed branch, and an `each` loop returns a new collection containing the result of each iteration. + +<!-- end list --> + +```lil +# Functional approach using a higher-order function +on twice f x do + f[f[x]] +end + +on double x do + x * 2 +end + +result: twice[double 10] # result is 40 +``` + +### Declarative (Query-based) Programming + +For data manipulation, Lil provides a powerful declarative query engine that resembles SQL. Instead of describing *how* to loop through and filter data, you declare *what* data you want. + + - **Queries:** Use `select`, `update`, and `extract` to query tables (and other collection types). + - **Clauses:** Filter, group, and sort data with `where`, `by`, and `orderby` clauses. + - **Readability:** Queries often result in more concise and readable code for data transformation tasks compared to imperative loops. + +<!-- end list --> + +```lil +# Declarative query to find developers +people: insert name age job with + "Alice" 25 "Development" + "Sam" 28 "Sales" + "Thomas" 40 "Development" +end + +devs: select name from people where job="Development" +# devs is now a table with the names "Alice" and "Thomas" +``` + +### Vector-Oriented Programming + +Influenced by languages like APL and K, this paradigm focuses on applying operations to entire arrays or lists (vectors) at once, a concept known as **conforming**. + + - **Conforming Operators:** Standard arithmetic operators (`+`, `-`, `*`, `/`) work element-wise on lists. + - **Efficiency:** Vector operations are significantly more performant than writing equivalent imperative loops. + - **The `@` Operator:** The "apply" operator (`@`) can be used to apply a function to each element of a list or to select multiple elements from a list by index. + +<!-- end list --> + +```lil +# Vector-oriented approach to add 10 to each number +numbers: [10, 20, 30] +result: numbers + 10 # result is [20, 30, 40] +``` + +----- + +## Key Principles + + - **Right-to-Left Evaluation:** Expressions are evaluated from right to left unless overridden by parentheses `()`. This is a fundamental rule that affects how all expressions are composed. + - **Copy-on-Write Immutability:** Lists, Dictionaries, and Tables are immutable. Operations like `update` or indexed assignments on an expression `(foo)[1]:44` return a new value, leaving the original unchanged. Direct assignment `foo[1]:44` is required to modify the variable `foo` itself. + - **Data-Centric Design:** The language provides powerful, built-in tools for data manipulation, especially through its query engine and vector operations. + - **Lexical Scoping:** Variables are resolved based on their location in the code's structure. Functions "close over" variables from their containing scope, enabling patterns like counters and encapsulated state. + +----- + +## Implementation/Usage + +The true power of Lil emerges when you mix these paradigms to solve problems cleanly and efficiently. + +### Basic Example + +Here, we combine an imperative loop with a vector-oriented operation to process a list of lists. + +```lil +# Calculate the magnitude of several 2D vectors +vectors: [[3,4], [5,12], [8,15]] +magnitudes: [] + +# Imperative loop over the list of vectors +each v in vectors do + # mag is a vector-oriented unary operator + magnitudes: magnitudes & [mag v] +end + +# magnitudes is now [5, 13, 17] +``` + +### Advanced Example + +This example defines a functional-style utility function (`avg`) and uses it within a declarative query to summarize data, an approach common in data analysis. + +```lil +# Functional helper function +on avg x do + (sum x) / count x +end + +# A table of sales data +sales: insert product category price with + "Apple" "Fruit" 0.5 + "Banana" "Fruit" 0.4 + "Bread" "Grain" 2.5 + "Rice" "Grain" 3.0 +end + +# Declarative query that uses the functional helper +avgPriceByCategory: select category:first category avg_price:avg[price] by category from sales + +# avgPriceByCategory is now: +# +----------+-----------+ +# | category | avg_price | +# +----------+-----------+ +# | "Fruit" | 0.45 | +# | "Grain" | 2.75 | +# +----------+-----------+ +``` + +----- + +## Common Patterns + +### Pattern 1: Query over Loop + +Instead of manually iterating with `each` to filter or transform a collection, use a declarative `select` or `extract` query. This is more concise, often faster, and less error-prone. + +```lil +# Instead of this imperative loop... +high_scores: [] +scores: [88, 95, 72, 100, 91] +each s in scores do + if s > 90 then + high_scores: high_scores & [s] + end +end + +# ...use a declarative query. +high_scores: extract value where value > 90 from scores +# high_scores is now [95, 100, 91] +``` + +### Pattern 2: Function Application with `@` + +For simple element-wise transformations on a list, using the `@` operator with a function is cleaner than writing an `each` loop. + +```lil +# Instead of this... +names: ["alice", "bob", "charlie"] +capitalized: [] +on capitalize s do first s & (1 drop s) end # Simple capitalize, for demo +each n in names do + capitalized: capitalized & [capitalize n] +end + +# ...use the more functional and concise @ operator. +on capitalize s do first s & (1 drop s) end +capitalized: capitalize @ names +# capitalized is now ["Alice", "Bob", "Charlie"] +``` + +----- + +## Best Practices + + - **Embrace Queries:** For any non-trivial data filtering, grouping, or transformation, reach for the query engine first. + - **Use Vector Operations:** When performing arithmetic or logical operations on lists, use conforming operators (`+`, `<`, `=`) instead of loops for better performance and clarity. + - **Distinguish Equality:** Use the conforming equals `=` within query expressions. Use the non-conforming match `~` in `if` or `while` conditions to avoid accidentally getting a list result. + - **Encapsulate with Functions:** Use functions to create reusable components and manage scope, especially for complex logic within Decker event handlers. + +----- + +## Common Pitfalls + + - **Right-to-Left Confusion:** Forgetting that `3*2+5` evaluates to `21`, not `11`. Use parentheses `(3*2)+5` to enforce the desired order of operations. + - **Expecting Mutation:** Believing that `update ... from my_table` changes `my_table`. It returns a *new* table. You must reassign it: `my_table: update ... from my_table`. + - **Comma as Argument Separator:** Writing `myfunc[arg1, arg2]`. This creates a list of two items and passes it as a single argument. The correct syntax is `myfunc[arg1 arg2]`. + - **Using `=` in `if`:** Writing `if some_list = some_value` can produce a list of `0`s and `1`s. An empty list `()` is falsey, but a list like `[0,0]` is truthy. Use `~` for a single boolean result in control flow. + +----- + +## Performance Considerations + +Vector-oriented algorithms are significantly faster and more memory-efficient than their imperative, element-by-element counterparts. The Lil interpreter is optimized for these bulk operations. For example, replacing values in a list using a calculated mask is preferable to an `each` loop with a conditional inside. + +```lil +# Slow, iterative approach +x: [1, 10, 2, 20, 3, 30] +result: each v in x + if v < 5 99 else v end +end + +# Fast, vector-oriented approach +mask: x < 5 # results in [1,0,1,0,1,0] +result: (99 * mask) + (x * !mask) +``` + +----- + +## Integration Points + +The primary integration point for Lil is **Decker**. Lil scripts are attached to Decker widgets, cards, and the deck itself to respond to user events (`on click`, `on keydown`, etc.). All paradigms are useful within Decker: + + - **Imperative:** To sequence actions, like showing a dialog and then navigating to another card. + - **Declarative:** To query data stored in a `grid` widget or to find specific cards in the deck, e.g., `extract value where value..widgets.visited.value from deck.cards`. + - **Functional/Vector:** To process data before displaying it, without needing slow loops. + +----- + +## Troubleshooting + +### Problem 1: An `if` statement behaves unpredictably with list comparisons. + + - **Symptoms:** An `if` block either never runs or always runs when comparing a value against a list. + - **Solution:** You are likely using the conforming equals operator (`=`), which returns a list of boolean results. In a conditional, you almost always want the non-conforming match operator (`~`), which returns a single `1` or `0`. + +### Problem 2: A recursive function crashes with a stack overflow on large inputs. + + - **Symptoms:** The script terminates unexpectedly when a recursive function is called with a large number or deep data structure. + - **Solution:** Lil supports **tail-call elimination**. Ensure your recursive call is the very last operation performed in the function. If it's part of a larger expression (e.g., `1 + my_func[...]`), it is not in a tail position. Rewrite the function to accumulate its result in an argument. + +----- + +## Examples in Context + +**Use Case: A Simple To-Do List in Decker** + +Imagine a Decker card with a `grid` widget named "tasks" (with columns "desc" and "done") and a `field` widget named "summary". + +```lil +# In the script of the "tasks" grid, to update when it's changed: +on change do + # Use a DECLARATIVE query to get the done/total counts. + # The query source is "me.value", the table in the grid. + stats: extract done:sum done total:count done from me.value + + # Use IMPERATIVE assignment to update the summary field. + summary.text: format["%d of %d tasks complete.", stats.done, stats.total] +end +``` + +This tiny script uses a declarative query to read the state and an imperative command to update the UI, demonstrating a practical mix of paradigms. diff --git a/bash/talk-to-computer/corpus/science/physics_basics.txt b/bash/talk-to-computer/corpus/science/physics_basics.txt new file mode 100644 index 0000000..5ae092b --- /dev/null +++ b/bash/talk-to-computer/corpus/science/physics_basics.txt @@ -0,0 +1,94 @@ +PHYSICS BASICS - Core Concepts and Principles + +CLASSICAL MECHANICS +================== + +Newton's Laws of Motion: +1. An object at rest stays at rest, and an object in motion stays in motion with the same speed and direction unless acted upon by an unbalanced force. (Inertia) + +2. Force equals mass times acceleration (F = ma) + +3. For every action, there is an equal and opposite reaction. + +Key Equations: +- Distance: d = vt (constant velocity) +- Velocity: v = v0 + at (constant acceleration) +- Distance with acceleration: d = v0t + (1/2)at² +- Momentum: p = mv +- Kinetic Energy: KE = (1/2)mv² +- Potential Energy: PE = mgh + +ELECTRICITY AND MAGNETISM +========================= + +Basic Concepts: +- Charge: Fundamental property of matter (positive/negative) +- Electric field: Force per unit charge +- Current: Rate of charge flow (I = Q/t) +- Voltage: Electric potential difference (V = IR) +- Resistance: Opposition to current flow + +Key Equations: +- Ohm's Law: V = IR +- Power: P = IV = I²R = V²/R +- Energy: E = Pt = VIt + +THERMODYNAMICS +============= + +Laws of Thermodynamics: +1. Energy conservation - energy cannot be created or destroyed +2. Entropy always increases in isolated systems +3. Absolute zero is unattainable +4. Entropy and temperature relationship + +Key Concepts: +- Heat transfer: Conduction, convection, radiation +- Specific heat capacity: Energy required to change temperature +- Phase changes: Melting, freezing, boiling, condensation +- Ideal gas law: PV = nRT + +QUANTUM PHYSICS +============== + +Core Principles: +- Wave-particle duality: Particles can exhibit wave-like behavior +- Uncertainty principle: Cannot know both position and momentum precisely +- Quantization: Energy levels are discrete, not continuous +- Superposition: Quantum systems can exist in multiple states + +Key Equations: +- Energy of photon: E = hf (h = Planck's constant) +- de Broglie wavelength: λ = h/p +- Heisenberg uncertainty: ΔxΔp ≥ h/4π + +MODERN PHYSICS +============= + +Special Relativity: +- Speed of light is constant for all observers +- Time dilation: Moving clocks run slower +- Length contraction: Moving objects appear shorter +- Mass-energy equivalence: E = mc² + +General Relativity: +- Gravity as curvature of spacetime +- Equivalence principle: Gravitational and inertial mass are identical +- Black holes and gravitational waves + +PRACTICAL APPLICATIONS +==================== + +Real-world Physics: +- Engineering: Bridges, buildings, vehicles +- Medicine: Imaging, radiation therapy, medical devices +- Technology: Computers, smartphones, satellites +- Energy: Solar panels, wind turbines, nuclear power + +Measurement Units: +- Length: meter (m) +- Mass: kilogram (kg) +- Time: second (s) +- Force: newton (N) = kg·m/s² +- Energy: joule (J) = N·m +- Power: watt (W) = J/s diff --git a/bash/talk-to-computer/corpus/topic_template.md b/bash/talk-to-computer/corpus/topic_template.md new file mode 100644 index 0000000..2ea9653 --- /dev/null +++ b/bash/talk-to-computer/corpus/topic_template.md @@ -0,0 +1,39 @@ +# Topic Name - Comprehensive Guide + +## Introduction + +[Brief introduction to the topic and its importance] + +## Core Concepts + +### [Main Concept 1] +[Explanation and details] + +### [Main Concept 2] +[Explanation and details] + +## Key Principles + +[Important principles and rules] + +## Examples + +[Code examples, diagrams, or practical applications] + +## Best Practices + +[Recommended approaches and common pitfalls] + +## References + +[Links to additional resources and further reading] + +--- + +**Template Instructions:** +1. Replace [bracketed text] with actual content +2. Add code blocks using markdown syntax +3. Include examples and practical applications +4. Save as .md, .txt, or .html file +5. Run `./corpus_manager.sh update` to refresh registry +6. Test with corpus queries diff --git a/bash/talk-to-computer/corpus_manager.sh b/bash/talk-to-computer/corpus_manager.sh new file mode 100755 index 0000000..47c743c --- /dev/null +++ b/bash/talk-to-computer/corpus_manager.sh @@ -0,0 +1,303 @@ +#!/bin/bash + +# Corpus Manager - Manages RAG corpus discovery and maintenance +# This script provides utilities for managing the knowledge corpus + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CORPUS_DIR="${SCRIPT_DIR}/corpus" +REGISTRY_FILE="${CORPUS_DIR}/corpus_registry.txt" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# --- Corpus Discovery Functions --- + +discover_corpus() { + echo -e "${BLUE}🔍 Discovering corpus structure...${NC}" + + # Find all directories under corpus/ + find "$CORPUS_DIR" -type d -mindepth 1 | while read -r dir; do + local topic_name=$(basename "$dir") + local parent_topic=$(basename "$(dirname "$dir")") + + # Skip if this is the corpus root + if [ "$parent_topic" = "corpus" ]; then + echo "Found topic directory: $topic_name" + fi + done +} + +# Generate topic keywords based on directory name and content +generate_topic_keywords() { + local topic_name="$1" + local keywords="" + + case "$topic_name" in + "programming") + keywords="bash,shell,scripting,programming,lil,algorithm,code,software,development" + ;; + "science") + keywords="physics,chemistry,biology,science,research,scientific" + ;; + "literature") + keywords="books,authors,literature,writing,analysis" + ;; + "lil") + keywords="decker,lil,language,programming,scripting,terse,deck" + ;; + "physics") + keywords="quantum,relativity,physics,mechanics,thermodynamics" + ;; + *) + # Generate keywords from directory name + keywords=$(echo "$topic_name" | sed 's/[-_]/,/g') + ;; + esac + + echo "$keywords" +} + +# Update the corpus registry +update_registry() { + echo -e "${BLUE}📝 Updating corpus registry...${NC}" + + # Backup existing registry + if [ -f "$REGISTRY_FILE" ]; then + cp "$REGISTRY_FILE" "${REGISTRY_FILE}.backup" + fi + + # Create new registry header + cat > "$REGISTRY_FILE" << 'EOF' +# Corpus Registry - Auto-generated by corpus_manager.sh +# Format: TOPIC|PATH|KEYWORDS|DESCRIPTION +# This file is automatically maintained - do not edit manually + +EOF + + # Find all directories and generate registry entries + find "$CORPUS_DIR" -type d -mindepth 1 | sort | while read -r dir; do + local topic_name=$(basename "$dir") + local relative_path="${dir#${SCRIPT_DIR}/}" + local keywords=$(generate_topic_keywords "$topic_name") + local description="$(echo "${topic_name:0:1}" | tr '[:lower:]' '[:upper:]')${topic_name:1} topics and resources" + + # Determine parent topic for hierarchical structure + local parent_dir=$(dirname "$dir") + local parent_topic="" + + if [ "$parent_dir" != "$CORPUS_DIR" ]; then + parent_topic=$(basename "$parent_dir") + description="$(echo "${topic_name:0:1}" | tr '[:lower:]' '[:upper:]')${topic_name:1} subset of ${parent_topic}" + fi + + # Add to registry + echo "${parent_topic:-$topic_name}|$relative_path|$keywords|$description" >> "$REGISTRY_FILE" + done + + echo -e "${GREEN}✅ Registry updated successfully${NC}" +} + +# --- Corpus Query Functions --- + +# Check if corpus exists for a given topic +corpus_exists() { + local topic="$1" + grep -q "^[^|]*${topic}|" "$REGISTRY_FILE" 2>/dev/null + return $? +} + +# Get corpus path for a topic +get_corpus_path() { + local topic="$1" + grep "^[^|]*${topic}|" "$REGISTRY_FILE" | head -1 | cut -d'|' -f2 +} + +# Get corpus keywords for a topic +get_corpus_keywords() { + local topic="$1" + grep "^[^|]*${topic}|" "$REGISTRY_FILE" | head -1 | cut -d'|' -f3 +} + +# List all available topics +list_topics() { + echo -e "${BLUE}📚 Available Corpus Topics:${NC}" + echo "----------------------------------------" + + if [ ! -f "$REGISTRY_FILE" ]; then + echo -e "${RED}No corpus registry found. Run 'update' first.${NC}" + return 1 + fi + + awk -F'|' 'NR>3 {print "• " $1 "/" $2 " - " $4}' "$REGISTRY_FILE" | sort +} + +# --- Corpus Content Functions --- + +# Count files in a corpus directory +count_corpus_files() { + local topic="$1" + local corpus_path=$(get_corpus_path "$topic") + + if [ -d "$corpus_path" ]; then + find "$corpus_path" -type f \( -name "*.txt" -o -name "*.md" -o -name "*.html" \) | wc -l + else + echo "0" + fi +} + +# Get corpus file list +list_corpus_files() { + local topic="$1" + local corpus_path=$(get_corpus_path "$topic") + + if [ -d "$corpus_path" ]; then + echo -e "${BLUE}📄 Files in $topic corpus:${NC}" + find "$corpus_path" -type f \( -name "*.txt" -o -name "*.md" -o -name "*.html" \) | sort + else + echo -e "${RED}Corpus directory not found: $corpus_path${NC}" + fi +} + +# --- Template and Setup Functions --- + +# Create template files for a new topic +create_topic_template() { + local topic="$1" + local corpus_path="$CORPUS_DIR/$topic" + + echo -e "${BLUE}🛠️ Creating template for topic: $topic${NC}" + + # Create directory if it doesn't exist + mkdir -p "$corpus_path" + + # Create template files + cat > "$corpus_path/README.md" << EOF +# $topic Corpus + +This directory contains documentation and resources for $topic. + +## File Format Guidelines + +- Use **Markdown (.md)** for structured content with headers +- Use **Plain text (.txt)** for simple notes and documentation +- Use **HTML (.html)** for rich content and formatting +- File names should be descriptive: \`topic_concept_name.md\` + +## Content Organization + +- Group related concepts in single files +- Use clear, descriptive headers +- Include code examples where relevant +- Add cross-references between related topics + +## Adding New Content + +1. Create new .md, .txt, or .html files in this directory +2. Run \`./corpus_manager.sh update\` to update the registry +3. Test with corpus queries +EOF + + cat > "$corpus_path/example.md" << EOF +# Example $topic Content + +This is an example file showing the expected format for $topic content. + +## Introduction + +Add your content here using standard Markdown formatting. + +## Key Concepts + +- Concept 1 +- Concept 2 +- Concept 3 + +## Examples + +\`\`\`bash +# Code examples go here +echo "Hello, $topic!" +\`\`\` + +## References + +- Link to relevant resources +- Additional reading materials +EOF + + echo -e "${GREEN}✅ Template created in: $corpus_path${NC}" + echo -e "${YELLOW}💡 Tip: Edit the files and run 'update' to refresh the registry${NC}" +} + +# --- Main Command Interface --- + +case "${1:-help}" in + "discover") + discover_corpus + ;; + "update") + update_registry + ;; + "list") + list_topics + ;; + "files") + if [ -n "$2" ]; then + list_corpus_files "$2" + else + echo -e "${RED}Usage: $0 files <topic>${NC}" + fi + ;; + "count") + if [ -n "$2" ]; then + local count=$(count_corpus_files "$2") + echo -e "${BLUE}📊 $2 corpus has $count files${NC}" + else + echo -e "${RED}Usage: $0 count <topic>${NC}" + fi + ;; + "template") + if [ -n "$2" ]; then + create_topic_template "$2" + else + echo -e "${RED}Usage: $0 template <topic>${NC}" + fi + ;; + "exists") + if [ -n "$2" ]; then + if corpus_exists "$2"; then + echo -e "${GREEN}✅ Corpus exists for topic: $2${NC}" + else + echo -e "${RED}❌ No corpus found for topic: $2${NC}" + fi + else + echo -e "${RED}Usage: $0 exists <topic>${NC}" + fi + ;; + "help"|*) + echo -e "${BLUE}📚 Corpus Manager${NC}" + echo "Manage the RAG knowledge corpus" + echo "" + echo -e "${YELLOW}Usage: $0 <command> [arguments]${NC}" + echo "" + echo "Commands:" + echo " discover Discover corpus structure" + echo " update Update corpus registry" + echo " list List all available topics" + echo " files <topic> List files in a topic corpus" + echo " count <topic> Count files in a topic corpus" + echo " exists <topic> Check if corpus exists for topic" + echo " template <topic> Create template files for new topic" + echo " help Show this help message" + echo "" + echo "Examples:" + echo " $0 update" + echo " $0 list" + echo " $0 template physics" + echo " $0 exists programming" + ;; +esac diff --git a/bash/talk-to-computer/corpus_prompt_template.md b/bash/talk-to-computer/corpus_prompt_template.md new file mode 100644 index 0000000..f4bb91e --- /dev/null +++ b/bash/talk-to-computer/corpus_prompt_template.md @@ -0,0 +1,125 @@ +You are an expert technical writer and subject matter expert. Your task is to create comprehensive, accurate, and well-structured documentation for a RAG (Retrieval-Augmented Generation) knowledge corpus. + +**TOPIC:** [SPECIFIC_TOPIC_NAME] +**DOMAIN:** [GENERAL_DOMAIN] (e.g., programming, science, literature, technology) +**TARGET_AUDIENCE:** [BEGINNER/INTERMEDIATE/ADVANCED] +**CONTENT_TYPE:** [CONCEPTS/REFERENCE/GUIDE/TUTORIAL] + +**REQUIREMENTS:** + +1. **Format**: Write in clean Markdown with proper headers (# ## ###) +2. **Structure**: Follow the established corpus content structure +3. **Accuracy**: Ensure technical accuracy and completeness +4. **Clarity**: Use clear, concise language with examples +5. **Searchability**: Include key terms and concepts that users might search for +6. **Cross-references**: Mention related topics where relevant + +**OUTPUT STRUCTURE:** + +# [TOPIC_NAME] - [BRIEF_DESCRIPTION] + +## Introduction + +[Provide a clear, concise introduction to the topic. Explain what it is, why it's important, and its main applications.] + +## Core Concepts + +### [Main Concept 1] +[Detailed explanation with examples] + +### [Main Concept 2] +[Detailed explanation with examples] + +## Key Principles + +[List and explain the fundamental principles or rules] + +## Implementation/Usage + +[Show how to apply the concepts with practical examples] + +### Basic Example +```language +// Code examples in appropriate languages +[EXAMPLE_CODE] +``` + +### Advanced Example +```language +// More complex implementation +[EXAMPLE_CODE] +``` + +## Common Patterns + +### Pattern 1: [Name] +[Description and when to use] + +### Pattern 2: [Name] +[Description and when to use] + +## Best Practices + +[Guidelines for effective usage] + +## Common Pitfalls + +[Things to avoid and how to recognize problems] + +## Performance Considerations + +[Performance implications and optimization tips] + +## Integration Points + +[How this integrates with related technologies/concepts] + +## Troubleshooting + +### Problem 1: [Common Issue] +**Symptoms:** [What to look for] +**Solution:** [How to fix] + +### Problem 2: [Common Issue] +**Symptoms:** [What to look for] +**Solution:** [How to fix] + +## Examples in Context + +[Real-world examples and use cases] + +## References + +[Links to official documentation, standards, or additional resources] + +## Related Topics + +- [Related Topic 1] +- [Related Topic 2] +- [Related Topic 3] + +--- + +**CONTENT GUIDELINES:** + +- Use **bold** for emphasis on key terms +- Use `inline code` for technical terms, function names, commands +- Use code blocks with syntax highlighting for examples +- Include both simple and complex examples +- Provide practical, actionable information +- Focus on clarity over complexity +- Include error cases and solutions +- Make content searchable with relevant keywords +- Structure content for easy navigation +- Ensure examples are complete and runnable + +**QUALITY CHECKS:** + +- [ ] Content is technically accurate +- [ ] Examples are complete and correct +- [ ] Structure follows corpus guidelines +- [ ] Headers are descriptive and hierarchical +- [ ] Cross-references are included where helpful +- [ ] Content is appropriate for target audience level +- [ ] Language is clear and professional +- [ ] All code examples are properly formatted \ No newline at end of file diff --git a/bash/critique b/bash/talk-to-computer/critique index 35a1db0..22a5fc6 100755 --- a/bash/critique +++ b/bash/talk-to-computer/critique @@ -38,6 +38,18 @@ # with each iteration potentially improving upon the previous response. # The system emphasizes quality improvement through structured feedback and revision. +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source the logging system +source "${SCRIPT_DIR}/logging.sh" + +# Source the quality guard for output quality protection +source "${SCRIPT_DIR}/quality_guard.sh" + +# Get mechanism name automatically +MECHANISM_NAME=$(get_mechanism_name "$0") + # --- Model Configuration --- RESPONSE_MODEL="llama3:8b-instruct-q4_K_M" CRITIC_MODEL="phi3:3.8b-mini-4k-instruct-q4_K_M" @@ -109,6 +121,7 @@ echo "Processing initial response..." # 2. The RESPONSE model generates the first answer RESPONSE_PROMPT="You are an expert, curious assistant who isn't afraid to say when they don't know something. Please respond directly to the following prompt: ${PROMPT}" RESPONSE_OUTPUT=$(ollama run "${RESPONSE_MODEL}" "${RESPONSE_PROMPT}") +RESPONSE_OUTPUT=$(guard_output_quality "$RESPONSE_OUTPUT" "$PROMPT" "$MECHANISM_NAME" "$RESPONSE_MODEL") # Append the response to the session file echo "INITIAL RESPONSE (${RESPONSE_MODEL}):" >> "${SESSION_FILE}" @@ -126,6 +139,7 @@ for i in $(seq 1 "${LOOPS}"); do # 3. The CRITIC model reviews the last response CRITIC_PROMPT="You are a detail oriented, close reading, keenly critical reviewer. Your task is to raise questions, flag potential misunderstandings, and areas for improved clarity in the following text. Provide concise, constructive criticism. Do not rewrite the text, only critique it. TEXT TO CRITIQUE: ${CURRENT_RESPONSE}" CRITIC_OUTPUT=$(ollama run "${CRITIC_MODEL}" "${CRITIC_PROMPT}") + CRITIC_OUTPUT=$(guard_output_quality "$CRITIC_OUTPUT" "$PROMPT" "$MECHANISM_NAME" "$CRITIC_MODEL") # Append the critique to the session file echo "CRITICISM ${i} (${CRITIC_MODEL}):" >> "${SESSION_FILE}" @@ -135,6 +149,7 @@ for i in $(seq 1 "${LOOPS}"); do # 4. The REFINE model reads the original prompt and the critique to generate a new response REFINE_PROMPT="You are an expert assistant. Your previous response was reviewed and critiqued. Your task now is to generate a refined, improved response to the original prompt based on the feedback provided. ORIGINAL PROMPT: ${PROMPT} CONSTRUCTIVE CRITICISM: ${CRITIC_OUTPUT} Generate the refined response now." REFINE_OUTPUT=$(ollama run "${REFINE_MODEL}" "${REFINE_PROMPT}") + REFINE_OUTPUT=$(guard_output_quality "$REFINE_OUTPUT" "$PROMPT" "$MECHANISM_NAME" "$REFINE_MODEL") # Append the refined response to the session file echo "REFINED RESPONSE ${i} (${REFINE_MODEL}):" >> "${SESSION_FILE}" diff --git a/bash/exploration b/bash/talk-to-computer/exploration index 8dc09b7..ff62a31 100755 --- a/bash/exploration +++ b/bash/talk-to-computer/exploration @@ -44,10 +44,38 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Source the logging system using absolute path source "${SCRIPT_DIR}/logging.sh" +# Source the quality guard for output quality protection +source "${SCRIPT_DIR}/quality_guard.sh" + +# Source the RAG integration for corpus queries +source "${SCRIPT_DIR}/rag_integration.sh" + +# Get mechanism name automatically +MECHANISM_NAME=$(get_mechanism_name "$0") + +# Ensure validation functions are available +if ! command -v validate_prompt >/dev/null 2>&1; then + echo "Error: Validation functions not loaded properly" >&2 + exit 1 +fi + # --- Model Configuration --- EXPLORATION_MODEL="llama3:8b-instruct-q4_K_M" ANALYSIS_MODEL="phi3:3.8b-mini-4k-instruct-q4_K_M" +# Validate and set models with fallbacks +EXPLORATION_MODEL=$(validate_model "$EXPLORATION_MODEL" "gemma3n:e2b") +if [ $? -ne 0 ]; then + log_error "No valid exploration model available" + exit 1 +fi + +ANALYSIS_MODEL=$(validate_model "$ANALYSIS_MODEL" "gemma3n:e2b") +if [ $? -ne 0 ]; then + log_error "No valid analysis model available" + exit 1 +fi + # --- Defaults --- DEFAULT_PATHS=3 @@ -76,7 +104,7 @@ while getopts "f:p:" opt; do NUM_PATHS="$OPTARG" ;; *) - echo "Invalid option: -$OPTARG" >&2 + log_error "Invalid option: -$OPTARG" exit 1 ;; esac @@ -90,16 +118,33 @@ else ROUNDS=$2 fi -# If file path is provided, append its contents to the prompt +# Validate prompt +PROMPT=$(validate_prompt "$PROMPT") +if [ $? -ne 0 ]; then + exit 1 +fi + +# Validate file path if provided if [ -n "$FILE_PATH" ]; then - if [ ! -f "$FILE_PATH" ]; then - echo "File not found: $FILE_PATH" >&2 + if ! validate_file_path "$FILE_PATH"; then exit 1 fi FILE_CONTENTS=$(cat "$FILE_PATH") PROMPT="$PROMPT\n[FILE CONTENTS]\n$FILE_CONTENTS\n[END FILE]" fi +# Validate number of paths +if ! [[ "$NUM_PATHS" =~ ^[1-9][0-9]*$ ]] || [ "$NUM_PATHS" -gt 10 ]; then + log_error "Invalid number of paths: $NUM_PATHS (must be 1-10)" + exit 1 +fi + +# Validate rounds +if ! [[ "$ROUNDS" =~ ^[1-9][0-9]*$ ]] || [ "$ROUNDS" -gt 5 ]; then + log_error "Invalid number of rounds: $ROUNDS (must be 1-5)" + exit 1 +fi + # --- File Initialization --- mkdir -p ~/tmp SESSION_FILE=~/tmp/exploration_$(date +%Y%m%d_%H%M%S).txt @@ -120,10 +165,20 @@ echo "" >> "${SESSION_FILE}" echo "Phase 1: Generating solution paths..." echo "PHASE 1 - PATH GENERATION:" >> "${SESSION_FILE}" +# Check for RAG context +RAG_CONTEXT=$(use_rag_if_available "${PROMPT}" "${MECHANISM_NAME}") + PATH_GENERATION_PROMPT="You are a strategic thinker. Your task is to identify ${NUM_PATHS} distinct, viable approaches to the following problem. Each path should represent a different strategy or methodology. PROBLEM: ${PROMPT} +$(if [[ "$RAG_CONTEXT" != "$PROMPT" ]]; then +echo "ADDITIONAL CONTEXT FROM KNOWLEDGE BASE: +$RAG_CONTEXT + +Use this context to inform your solution paths and provide more relevant alternatives." +fi) + Please identify ${NUM_PATHS} different solution paths. For each path, provide: 1. A clear name/title for the approach 2. A brief description of the strategy @@ -141,6 +196,7 @@ etc. Ensure the paths are genuinely different approaches, not just variations of the same idea." paths_output=$(ollama run "${EXPLORATION_MODEL}" "${PATH_GENERATION_PROMPT}") +paths_output=$(guard_output_quality "$paths_output" "$PROMPT" "$MECHANISM_NAME" "$EXPLORATION_MODEL") echo "GENERATED PATHS:" >> "${SESSION_FILE}" echo "${paths_output}" >> "${SESSION_FILE}" @@ -184,6 +240,7 @@ Please provide a comprehensive analysis of this path including: Provide a thorough, well-structured analysis." path_analysis=$(ollama run "${EXPLORATION_MODEL}" "${EXPLORATION_PROMPT}") + path_analysis=$(guard_output_quality "$path_analysis" "$PROMPT" "$MECHANISM_NAME" "$EXPLORATION_MODEL") path_analyses[$((i-1))]="${path_analysis}" path_names[$((i-1))]="${path_name}" @@ -225,6 +282,7 @@ Please provide a comprehensive comparative analysis including: Provide a clear, structured comparison that helps decision-making." comparative_analysis=$(ollama run "${ANALYSIS_MODEL}" "${COMPARISON_PROMPT}") +comparative_analysis=$(guard_output_quality "$comparative_analysis" "$PROMPT" "$MECHANISM_NAME" "$ANALYSIS_MODEL") echo "COMPARATIVE ANALYSIS:" >> "${SESSION_FILE}" echo "${comparative_analysis}" >> "${SESSION_FILE}" diff --git a/bash/talk-to-computer/lil_tester.sh b/bash/talk-to-computer/lil_tester.sh new file mode 100755 index 0000000..8bdcc41 --- /dev/null +++ b/bash/talk-to-computer/lil_tester.sh @@ -0,0 +1,288 @@ +#!/bin/bash + +# Lil Script Tester - Secure Sandbox Testing Module +# This module provides secure testing capabilities for Lil scripts generated by the puzzle mechanism. +# +# SECURITY FEATURES: +# - Sandboxed execution environment +# - Resource limits (CPU, memory, time) +# - File system isolation +# - Network access prevention +# - Safe error handling +# - Result validation and sanitization + +# --- Configuration --- +TEST_TIMEOUT=10 # Maximum execution time in seconds +MAX_OUTPUT_SIZE=10000 # Maximum output size in characters +TEMP_DIR_BASE="/tmp/lil_test" # Base temporary directory +SAFE_COMMANDS=("print" "echo" "count" "first" "last" "sum" "min" "max" "range" "keys" "list" "table" "typeof" "mag" "unit") + +# --- Security Functions --- + +# Create secure temporary directory +create_secure_temp_dir() { + local dir="$1" + mkdir -p "$dir" + chmod 700 "$dir" + + # Create a minimal environment + echo "()" > "$dir/empty.lil" + echo "nil" > "$dir/nil.lil" +} + +# Clean up temporary directory +cleanup_temp_dir() { + local dir="$1" + if [ -d "$dir" ]; then + rm -rf "$dir" 2>/dev/null + fi +} + +# Validate Lil code for potentially dangerous operations +validate_lil_code() { + local code="$1" + + # Check for potentially dangerous patterns + local dangerous_patterns=( + "system\\[" # System calls + "exec\\[" # Execution + "file\\." # File operations + "network\\." # Network operations + "http\\." # HTTP requests + "shell\\[" # Shell execution + "\\$\\(" # Command substitution + "\\`.*\\`" # Backtick execution + ) + + for pattern in "${dangerous_patterns[@]}"; do + if echo "$code" | grep -q "$pattern" 2>/dev/null; then + echo "DANGEROUS_CODE_DETECTED: $pattern" + return 1 + fi + done + + # Check for reasonable complexity (prevent infinite loops) + local line_count=$(echo "$code" | wc -l) + if [ "$line_count" -gt 100 ]; then + echo "CODE_TOO_COMPLEX: $line_count lines (max: 100)" + return 1 + fi + + echo "CODE_VALIDATED" + return 0 +} + +# Create a safe test wrapper +create_safe_test_wrapper() { + local code="$1" + local test_name="$2" + local temp_dir="$3" + + # Create a safe test file + cat > "$temp_dir/test_$test_name.lil" << EOF +# Safe test wrapper for: $test_name +# Generated by Lil Tester + +# Set safe defaults +on safe_test do + local result + local error_occurred + + # Wrap execution in error handling + on execute_safely do + $code + end + + # Execute and capture result + result:execute_safely() + + # Return result or error indicator + if result = nil + "ERROR: Execution failed or returned nil" + else + result + end +end + +# Run the test +safe_test() +EOF +} + +# Execute Lil code safely +execute_lil_safely() { + local code="$1" + local test_name="$2" + local temp_dir="$3" + + # Validate code first + local validation_result=$(validate_lil_code "$code") + if [ $? -ne 0 ]; then + echo "VALIDATION_FAILED: $validation_result" + return 1 + fi + + # Create safe test wrapper + create_safe_test_wrapper "$code" "$test_name" "$temp_dir" + + # Try lilt first, fallback to lila + local result="" + local exit_code=1 + + # Test with lilt + if command -v lilt >/dev/null 2>&1; then + echo "Testing with lilt..." + result=$(timeout "$TEST_TIMEOUT" lilt "$temp_dir/test_$test_name.lil" 2>&1) + exit_code=$? + + if [ $exit_code -eq 0 ]; then + echo "SUCCESS: lilt execution completed" + else + echo "lilt failed, trying lila..." + fi + fi + + # Fallback to lila if lilt failed + if [ $exit_code -ne 0 ] && command -v lila >/dev/null 2>&1; then + echo "Testing with lila..." + result=$(timeout "$TEST_TIMEOUT" lila "$temp_dir/test_$test_name.lil" 2>&1) + exit_code=$? + + if [ $exit_code -eq 0 ]; then + echo "SUCCESS: lila execution completed" + else + echo "Both lilt and lila failed" + fi + fi + + # Check output size + local output_size=${#result} + if [ "$output_size" -gt "$MAX_OUTPUT_SIZE" ]; then + result="$(echo "$result" | head -c "$MAX_OUTPUT_SIZE")... [TRUNCATED]" + fi + + echo "$result" + return $exit_code +} + +# Run comprehensive tests +run_lil_tests() { + local code="$1" + local test_name="$2" + + # Create unique temporary directory + local temp_dir="${TEMP_DIR_BASE}_$$_$(date +%s)" + + echo "=== Lil Script Testing ===" + echo "Test Name: $test_name" + echo "Code Length: $(echo "$code" | wc -c) characters" + echo "----------------------------------------" + + # Create secure temporary directory + create_secure_temp_dir "$temp_dir" + + # Trap cleanup on exit + trap 'cleanup_temp_dir "$temp_dir"' EXIT + + # Execute the code safely + local start_time=$(date +%s.%N) + local result=$(execute_lil_safely "$code" "$test_name" "$temp_dir") + local exit_code=$? + local end_time=$(date +%s.%N) + + # Calculate execution time + local duration=$(echo "$end_time - $start_time" | bc -l 2>/dev/null || echo "0") + + # Report results + echo "----------------------------------------" + echo "Test Results:" + echo "Exit Code: $exit_code" + echo "Execution Time: ${duration}s" + echo "Output:" + echo "$result" + + if [ $exit_code -eq 0 ]; then + echo "✅ Test PASSED" + return 0 + else + echo "❌ Test FAILED" + return 1 + fi +} + +# Test specific Lil constructs +test_lil_constructs() { + local code="$1" + local test_name="$2" + + # Create unique temporary directory for construct testing + local temp_dir="${TEMP_DIR_BASE}_constructs_$$_$(date +%s)" + + echo "=== Lil Construct Testing ===" + echo "Testing specific Lil language features..." + + # Create and cleanup temp dir + create_secure_temp_dir "$temp_dir" + trap 'cleanup_temp_dir "$temp_dir"' EXIT + + # Test basic operations + local basic_tests=( + "Basic arithmetic: 2+3*4" + "List operations: (1,2,3) take 2" + "Dictionary: dict (\"a\",1) (\"b\",2)" + "Function definition: on test do 42 end" + ) + + for test in "${basic_tests[@]}"; do + local test_desc=$(echo "$test" | cut -d: -f1) + local test_code=$(echo "$test" | cut -d: -f2) + + echo "Testing: $test_desc" + local result=$(execute_lil_safely "$test_code" "basic_$test_desc" "$temp_dir") + + if [ $? -eq 0 ]; then + echo " ✅ $test_desc: PASSED" + else + echo " ❌ $test_desc: FAILED" + fi + done +} + +# Main testing interface +test_lil_script() { + local code="$1" + local test_name="${2:-unnamed_test}" + + if [ -z "$code" ]; then + echo "Error: No code provided for testing" + return 1 + fi + + # Run the main test + run_lil_tests "$code" "$test_name" + local main_result=$? + + # Run construct-specific tests + test_lil_constructs "$code" "$test_name" + + return $main_result +} + +# Export functions for use by other scripts +export -f test_lil_script +export -f run_lil_tests +export -f execute_lil_safely +export -f validate_lil_code +export -f create_secure_temp_dir +export -f cleanup_temp_dir + +# If run directly, provide usage information +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + if [ "$#" -lt 1 ]; then + echo "Usage: $0 <lil_code> [test_name]" + echo "Example: $0 'on test do 42 end' 'simple_function'" + exit 1 + fi + + test_lil_script "$1" "${2:-unnamed_test}" +fi diff --git a/bash/logging.sh b/bash/talk-to-computer/logging.sh index c37aaf4..c8a61d1 100755 --- a/bash/logging.sh +++ b/bash/talk-to-computer/logging.sh @@ -7,10 +7,99 @@ LOG_DIR=~/tmp/ai_thinking METRICS_FILE="${LOG_DIR}/performance_metrics.json" SESSION_LOG="${LOG_DIR}/session_$(date +%Y%m%d_%H%M%S).json" +ERROR_LOG="${LOG_DIR}/errors.log" # Create logging directory mkdir -p "${LOG_DIR}" +# --- Error Logging --- +log_error() { + local message="$1" + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + echo "[ERROR] ${timestamp}: ${message}" >> "${ERROR_LOG}" + echo "Error: ${message}" >&2 +} + +log_warning() { + local message="$1" + local timestamp=$(date '+%Y-%m-%d %H:%M:%S') + echo "[WARNING] ${timestamp}: ${message}" >> "${ERROR_LOG}" + echo "Warning: ${message}" >&2 +} + +# --- Input Validation Functions --- +validate_file_path() { + local file_path="$1" + + if [ -z "$file_path" ]; then + return 0 # Empty path is valid (optional) + fi + + if [ ! -f "$file_path" ]; then + log_error "File not found: $file_path" + return 1 + fi + + if [ ! -r "$file_path" ]; then + log_error "File not readable: $file_path" + return 1 + fi + + return 0 +} + +validate_prompt() { + local prompt="$1" + local max_length=10000 + + if [ -z "$prompt" ] || [[ "$prompt" =~ ^[[:space:]]*$ ]]; then + log_error "Empty or whitespace-only prompt" + return 1 + fi + + if [ ${#prompt} -gt $max_length ]; then + log_error "Prompt too long (${#prompt} chars, max: $max_length)" + return 1 + fi + + # Basic sanitization - remove potentially dangerous characters + local sanitized=$(echo "$prompt" | sed 's/[<>"'\''&]/g' 2>/dev/null || echo "$prompt") + if [ "$sanitized" != "$prompt" ]; then + log_warning "Prompt contained special characters that were sanitized" + echo "$sanitized" + return 0 + fi + + echo "$prompt" + return 0 +} + +# --- Model Validation --- +validate_model() { + local model="$1" + local fallback_model="$2" + + if ! command -v ollama >/dev/null 2>&1; then + log_error "Ollama not found in PATH" + return 1 + fi + + if ! ollama list | grep -q "$model"; then + log_warning "Model '$model' not available" + if [ -n "$fallback_model" ] && ollama list | grep -q "$fallback_model"; then + log_warning "Falling back to '$fallback_model'" + echo "$fallback_model" + return 0 + else + log_error "No fallback model available" + return 1 + fi + fi + + echo "$model" + return 0 +} + # --- Timing Functions --- start_timer() { local session_id="$1" @@ -143,4 +232,16 @@ export -f log_session_start export -f log_session_end export -f update_metrics export -f generate_session_id -export -f get_metrics_summary \ No newline at end of file +export -f get_metrics_summary +export -f log_error +export -f log_warning +export -f validate_file_path +export -f validate_prompt +export -f validate_model + +# Alternative export method for compatibility +if [ -n "$BASH_VERSION" ]; then + export start_timer end_timer log_session_start log_session_end update_metrics + export generate_session_id get_metrics_summary log_error log_warning + export validate_file_path validate_prompt validate_model +fi \ No newline at end of file diff --git a/bash/metrics b/bash/talk-to-computer/metrics index ad430b5..ad430b5 100755 --- a/bash/metrics +++ b/bash/talk-to-computer/metrics diff --git a/bash/talk-to-computer/model_selector.sh b/bash/talk-to-computer/model_selector.sh new file mode 100755 index 0000000..d3a46a1 --- /dev/null +++ b/bash/talk-to-computer/model_selector.sh @@ -0,0 +1,380 @@ +#!/bin/bash + +# Dynamic Model Selector +# Intelligently selects models based on task type, availability, and capabilities + +# --- Model Capability Database --- + +# Model database using simple variables (compatible with older bash) +MODEL_DB_DIR="${LOG_DIR:-/tmp/ai_thinking}/model_db" + +# Initialize model database with simple variables +init_model_database() { + # Create model database directory + mkdir -p "$MODEL_DB_DIR" + + # Model capabilities by task type (using file-based storage for compatibility) + + # === CONFIGURED MODELS === + cat > "$MODEL_DB_DIR/llama3_8b_instruct_q4_K_M" << 'EOF' +coding=0.8 +reasoning=0.9 +creative=0.7 +size=8 +speed=0.8 +EOF + + cat > "$MODEL_DB_DIR/phi3_3_8b_mini_4k_instruct_q4_K_M" << 'EOF' +coding=0.7 +reasoning=0.8 +creative=0.6 +size=3.8 +speed=0.9 +EOF + + cat > "$MODEL_DB_DIR/deepseek_r1_1_5b" << 'EOF' +coding=0.6 +reasoning=0.9 +creative=0.5 +size=1.5 +speed=0.95 +EOF + + cat > "$MODEL_DB_DIR/gemma3n_e2b" << 'EOF' +coding=0.8 +reasoning=0.8 +creative=0.8 +size=2 +speed=0.85 +EOF + + cat > "$MODEL_DB_DIR/dolphin3_latest" << 'EOF' +coding=0.6 +reasoning=0.7 +creative=0.8 +size=7 +speed=0.7 +EOF + + # === ADDITIONAL MODELS FROM OLLAMA LIST === + + # Llama 3.1 - Newer version, should be similar to Llama 3 but potentially better + cat > "$MODEL_DB_DIR/llama3_1_8b" << 'EOF' +coding=0.82 +reasoning=0.92 +creative=0.72 +size=8 +speed=0.82 +EOF + + # DeepSeek R1 7B - Larger reasoning model + cat > "$MODEL_DB_DIR/deepseek_r1_7b" << 'EOF' +coding=0.65 +reasoning=0.95 +creative=0.55 +size=7 +speed=0.7 +EOF + + # Gemma 3N Latest - Larger version of e2b + cat > "$MODEL_DB_DIR/gemma3n_latest" << 'EOF' +coding=0.82 +reasoning=0.82 +creative=0.82 +size=7.5 +speed=0.8 +EOF + + # Gemma 3 4B - Different model family + cat > "$MODEL_DB_DIR/gemma3_4b" << 'EOF' +coding=0.75 +reasoning=0.78 +creative=0.75 +size=4 +speed=0.85 +EOF + + # Qwen2.5 7B - Alibaba model, general purpose + cat > "$MODEL_DB_DIR/qwen2_5_7b" << 'EOF' +coding=0.78 +reasoning=0.85 +creative=0.7 +size=7 +speed=0.75 +EOF + + # Qwen3 8B - Latest Qwen model + cat > "$MODEL_DB_DIR/qwen3_8b" << 'EOF' +coding=0.8 +reasoning=0.88 +creative=0.72 +size=8 +speed=0.78 +EOF + + # Qwen3 4B - Smaller Qwen model + cat > "$MODEL_DB_DIR/qwen3_4b" << 'EOF' +coding=0.75 +reasoning=0.82 +creative=0.68 +size=4 +speed=0.85 +EOF + + # Qwen3 1.7B - Smallest Qwen model + cat > "$MODEL_DB_DIR/qwen3_1_7b" << 'EOF' +coding=0.65 +reasoning=0.7 +creative=0.6 +size=1.7 +speed=0.95 +EOF + + # DeepScaler - Performance optimization focus + cat > "$MODEL_DB_DIR/deepscaler_latest" << 'EOF' +coding=0.7 +reasoning=0.8 +creative=0.65 +size=3.6 +speed=0.88 +EOF + + # Yasser Qwen2.5 - Fine-tuned variant + cat > "$MODEL_DB_DIR/yasserrmd_Qwen2_5_7B_Instruct_1M_latest" << 'EOF' +coding=0.82 +reasoning=0.9 +creative=0.75 +size=7 +speed=0.75 +EOF + + # Nomic Embed Text - Specialized for embeddings, not general tasks + cat > "$MODEL_DB_DIR/nomic_embed_text_latest" << 'EOF' +coding=0.1 +reasoning=0.1 +creative=0.1 +size=0.274 +speed=0.95 +EOF +} + +# Get model capability score +get_model_capability() { + local model_key="$1" + local task_type="$2" + + # Convert model name to filename-friendly format + local safe_name=$(echo "$model_key" | tr ':' '_' | tr '.' '_') + local db_file="$MODEL_DB_DIR/$safe_name" + + if [ -f "$db_file" ]; then + grep "^${task_type}=" "$db_file" | cut -d'=' -f2 + else + echo "0.5" # Default capability score + fi +} + +# Get model size +get_model_size() { + local model_key="$1" + local safe_name=$(echo "$model_key" | tr ':' '_' | tr '.' '_') + local db_file="$MODEL_DB_DIR/$safe_name" + + if [ -f "$db_file" ]; then + grep "^size=" "$db_file" | cut -d'=' -f2 + else + echo "5" # Default size + fi +} + +# Get model speed +get_model_speed() { + local model_key="$1" + local safe_name=$(echo "$model_key" | tr ':' '_' | tr '.' '_') + local db_file="$MODEL_DB_DIR/$safe_name" + + if [ -f "$db_file" ]; then + grep "^speed=" "$db_file" | cut -d'=' -f2 + else + echo "0.5" # Default speed + fi +} + +# --- Model Discovery --- + +# Get list of available models +get_available_models() { + ollama list 2>/dev/null | tail -n +2 | awk '{print $1}' | sort +} + +# Check if a model is available +is_model_available() { + local model="$1" + ollama list 2>/dev/null | grep -q "^${model}\s" +} + +# --- Task Type Classification --- + +# Classify task type from prompt and mechanism +classify_task_type() { + local prompt="$1" + local mechanism="$2" + + # Task type classification based on mechanism + case "$mechanism" in + "puzzle") + echo "coding" + ;; + "socratic") + echo "reasoning" + ;; + "exploration") + echo "reasoning" + ;; + "consensus") + echo "reasoning" + ;; + "critique") + echo "reasoning" + ;; + "synthesis") + echo "reasoning" + ;; + "peer-review") + echo "reasoning" + ;; + *) + # Fallback to keyword-based classification + if echo "$prompt" | grep -q -i "code\|algorithm\|function\|program\|implement"; then + echo "coding" + elif echo "$prompt" | grep -q -i "write\|story\|creative\|poem\|essay"; then + echo "creative" + else + echo "reasoning" + fi + ;; + esac +} + +# --- Model Selection Logic --- + +# Select best model for task +select_best_model() { + local task_type="$1" + local available_models="$2" + local preferred_models="$3" + + local best_model="" + local best_score=0 + + # First, try preferred models if available + if [ -n "$preferred_models" ]; then + for model in $preferred_models; do + if echo "$available_models" | grep -q "^${model}$" && is_model_available "$model"; then + local capability_score=$(get_model_capability "$model" "$task_type") + local speed_score=$(get_model_speed "$model") + local model_size=$(get_model_size "$model") + local size_score=$(echo "scale=2; $model_size / 10" | bc -l 2>/dev/null || echo "0.5") + + # Calculate weighted score (capability is most important) + local total_score=$(echo "scale=2; ($capability_score * 0.6) + ($speed_score * 0.3) + ($size_score * 0.1)" | bc -l 2>/dev/null || echo "0.5") + + if (( $(echo "$total_score > $best_score" | bc -l 2>/dev/null || echo "0") )); then + best_score=$total_score + best_model=$model + fi + fi + done + fi + + # If no preferred model is good, find best available model + if [ -z "$best_model" ]; then + for model in $available_models; do + if is_model_available "$model"; then + local capability_score=$(get_model_capability "$model" "$task_type") + local speed_score=$(get_model_speed "$model") + local model_size=$(get_model_size "$model") + local size_score=$(echo "scale=2; $model_size / 10" | bc -l 2>/dev/null || echo "0.5") + + local total_score=$(echo "scale=2; ($capability_score * 0.6) + ($speed_score * 0.3) + ($size_score * 0.1)" | bc -l 2>/dev/null || echo "0.5") + + if (( $(echo "$total_score > $best_score" | bc -l 2>/dev/null || echo "0") )); then + best_score=$total_score + best_model=$model + fi + fi + done + fi + + if [ -n "$best_model" ]; then + echo "Selected model: $best_model (score: $best_score, task: $task_type)" 1>&2 + echo "$best_model" + else + echo "No suitable model found" >&2 + echo "" + fi +} + +# --- Main Selection Function --- + +# Smart model selection +select_model_for_task() { + local prompt="$1" + local mechanism="$2" + local preferred_models="$3" + + # Initialize database + init_model_database + + # Get available models + local available_models + available_models=$(get_available_models) + + if [ -z "$available_models" ]; then + echo "No models available via Ollama" >&2 + echo "" + return 1 + fi + + # Classify task type + local task_type + task_type=$(classify_task_type "$prompt" "$mechanism") + + # Select best model + local selected_model + selected_model=$(select_best_model "$task_type" "$available_models" "$preferred_models") + + if [ -n "$selected_model" ]; then + echo "$selected_model" + return 0 + else + echo "" + return 1 + fi +} + +# --- Utility Functions --- + +# Get model info +get_model_info() { + local model="$1" + + echo "Model: $model" + echo "Size: $(get_model_size "$model")B" + echo "Speed: $(get_model_speed "$model")" + echo "Coding: $(get_model_capability "$model" "coding")" + echo "Reasoning: $(get_model_capability "$model" "reasoning")" + echo "Creative: $(get_model_capability "$model" "creative")" +} + +# Export functions +export -f init_model_database +export -f get_available_models +export -f is_model_available +export -f classify_task_type +export -f select_best_model +export -f select_model_for_task +export -f get_model_info +export -f get_model_capability +export -f get_model_size +export -f get_model_speed diff --git a/bash/peer-review b/bash/talk-to-computer/peer-review index 0a7be1a..e674375 100755 --- a/bash/peer-review +++ b/bash/talk-to-computer/peer-review @@ -40,6 +40,18 @@ # with each cycle potentially improving upon the previous version. # The system emphasizes collaborative improvement through structured feedback and revision. +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source the logging system +source "${SCRIPT_DIR}/logging.sh" + +# Source the quality guard for output quality protection +source "${SCRIPT_DIR}/quality_guard.sh" + +# Get mechanism name automatically +MECHANISM_NAME=$(get_mechanism_name "$0") + # --- Model Configuration --- MODELS=( "llama3:8b-instruct-q4_K_M" @@ -129,6 +141,7 @@ INITIAL_PROMPT="You are an expert assistant. Please provide a comprehensive resp PROMPT: ${PROMPT}" INITIAL_RESPONSE=$(ollama run "${AUTHOR_MODEL}" "${INITIAL_PROMPT}") +INITIAL_RESPONSE=$(guard_output_quality "$INITIAL_RESPONSE" "$PROMPT" "$MECHANISM_NAME" "$AUTHOR_MODEL") echo "INITIAL RESPONSE (${AUTHOR_MODEL}):" >> "${SESSION_FILE}" echo "${INITIAL_RESPONSE}" >> "${SESSION_FILE}" @@ -174,6 +187,7 @@ RESPONSE TO REVIEW: ${CURRENT_RESPONSE} Please provide your peer review feedback in a clear, structured format. Focus on actionable suggestions for improvement." review_output=$(ollama run "${model}" "${REVIEW_PROMPT}") + review_output=$(guard_output_quality "$review_output" "$PROMPT" "$MECHANISM_NAME" "$model") reviews[$review_count]="${review_output}" reviewer_names[$review_count]="${model}" @@ -211,6 +225,7 @@ Please provide a revised version of your response that: - Shows how you've responded to the peer review process" REFINED_RESPONSE=$(ollama run "${AUTHOR_MODEL}" "${REFINE_PROMPT}") + REFINED_RESPONSE=$(guard_output_quality "$REFINED_RESPONSE" "$PROMPT" "$MECHANISM_NAME" "$AUTHOR_MODEL") echo "REFINED RESPONSE (${AUTHOR_MODEL}):" >> "${SESSION_FILE}" echo "${REFINED_RESPONSE}" >> "${SESSION_FILE}" @@ -240,6 +255,7 @@ Please provide a summary that: - Is clear, concise, and well-organized" FINAL_SUMMARY=$(ollama run "${AUTHOR_MODEL}" "${SUMMARY_PROMPT}") +FINAL_SUMMARY=$(guard_output_quality "$FINAL_SUMMARY" "$PROMPT" "$MECHANISM_NAME" "$AUTHOR_MODEL") echo "FINAL SUMMARY (${AUTHOR_MODEL}):" >> "${SESSION_FILE}" echo "${FINAL_SUMMARY}" >> "${SESSION_FILE}" diff --git a/bash/talk-to-computer/puzzle b/bash/talk-to-computer/puzzle new file mode 100755 index 0000000..b9ab040 --- /dev/null +++ b/bash/talk-to-computer/puzzle @@ -0,0 +1,442 @@ +#!/bin/bash + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source the logging system using absolute path +source "${SCRIPT_DIR}/logging.sh" + +# Source the Lil tester for secure code testing +source "${SCRIPT_DIR}/lil_tester.sh" + +# Source the quality guard for output quality protection +source "${SCRIPT_DIR}/quality_guard.sh" + +# Source the RAG integration for corpus queries +source "${SCRIPT_DIR}/rag_integration.sh" + +# Get mechanism name automatically +MECHANISM_NAME=$(get_mechanism_name "$0") + +# --- Lil Knowledge Base --- +get_lil_knowledge() { + cat << 'EOF' +Lil is a multi-paradigm scripting language used in the Decker multimedia tool. Here is comprehensive knowledge about Lil: + +## Core Language Features + +### Types and Values +- **Numbers**: Floating-point values (42, 37.5, -29999) +- **Strings**: Double-quoted with escape sequences ("hello\nworld", "foo\"bar") +- **Lists**: Ordered sequences using comma: (1,2,3), empty list () +- **Dictionaries**: Key-value pairs: ("a","b") dict (11,22) or {}.fruit:"apple" +- **Tables**: Rectangular data with named columns (created with insert or table) +- **Functions**: Defined with 'on name do ... end', called with [] +- **Interfaces**: Opaque values for system resources + +### Basic Syntax +- **Variables**: Assignment uses ':' (x:42, y:"hello") +- **Indexing**: Lists with [index], dicts with .key or ["key"] +- **Operators**: Right-to-left precedence, 2*3+5 = 16, (2*3)+5 = 11 +- **Comments**: # line comments only +- **Expressions**: Everything is an expression, returns values + +### Control Flow +- **Conditionals**: if condition ... end, with optional elseif/else +- **Loops**: each value key index in collection ... end +- **While**: while condition ... end +- **Functions**: on func x y do ... end, called as func[args] + +### Query Language (SQL-like) +- **select**: select columns from table where condition orderby column +- **update**: update column:value from table where condition +- **extract**: extract values from table (returns simple types) +- **insert**: insert columns with values end +- **Clauses**: where, by, orderby asc/desc +- **Joins**: table1 join table2 (natural), table1 cross table2 (cartesian) + +### Vector Operations (Conforming) +- **Arithmetic spreads**: 1+(2,3,4) = (3,4,5) +- **List operations**: (1,2,3)+(4,5,6) = (5,7,9) +- **Equality**: 5=(1,5,10) = (0,1,0) # Use ~ for exact match +- **Application**: func @ (1,2,3) applies func to each element + +### Key Operators +- **Arithmetic**: + - * / % ^ & | (min/max) +- **Comparison**: < > = (conforming), ~ (exact match) +- **Logic**: ! (not), & | (and/or with numbers) +- **Data**: , (concat), @ (index each), dict, take, drop +- **String**: fuse (concat), split, format, parse, like (glob matching) +- **Query**: join, cross, limit, window + +### Important Patterns +- **Function definition**: on add x y do x+y end +- **List comprehension**: each x in data x*2 end +- **Table query**: select name age where age>21 from people +- **Dictionary building**: d:(); d.key:"value" +- **String formatting**: "%i %s" format (42,"answer") + +### Common Functions +- **Math**: cos, sin, tan, exp, ln, sqrt, floor, mag, unit, heading +- **Aggregation**: sum, prod, min, max, count, first, last +- **Data**: range, keys, list, table, flip, raze, typeof +- **IO**: read, write, show, print (in Lilt environment) + +### Best Practices +- Use functional style when possible (immutable operations) +- Leverage vector operations for data manipulation +- Use queries for complex data transformations +- Functions are first-class values +- Lexical scoping with closures +- Tail-call optimization supported + +### Common Patterns +- **Mode finding**: extract first value by value orderby count value desc from data +- **Filtering**: select from table where condition +- **Grouping**: select agg_func column by group_column from table +- **List processing**: each x in data transform[x] end +- **Dictionary operations**: keys dict, range dict, dict operations +- **String manipulation**: split, fuse, format, parse, like + +Lil emphasizes expressive, concise code with powerful built-in operations for data manipulation, making it excellent for algorithmic puzzles and data processing tasks. +EOF +} + +# --- Model Configuration --- +PUZZLE_MODEL="llama3:8b-instruct-q4_K_M" +ANALYSIS_MODEL="phi3:3.8b-mini-4k-instruct-q4_K_M" + +# Validate and set models with fallbacks +PUZZLE_MODEL=$(validate_model "$PUZZLE_MODEL" "gemma3n:e2b") +if [ $? -ne 0 ]; then + log_error "No valid puzzle model available" + exit 1 +fi + +ANALYSIS_MODEL=$(validate_model "$ANALYSIS_MODEL" "gemma3n:e2b") +if [ $? -ne 0 ]; then + log_error "No valid analysis model available" + exit 1 +fi + +# --- Defaults --- +DEFAULT_ROUNDS=2 + +# --- Argument Validation --- +if [ "$#" -lt 1 ]; then + echo -e "\n\tPuzzle" + echo -e "\tThis script specializes in puzzle solving and coding challenges with Lil programming language expertise." + echo -e "\n\tUsage: $0 [-f <file_path>] [-l <language>] \"<your puzzle/challenge>\" [number_of_rounds]" + echo -e "\n\tExample: $0 -f ./challenge.txt -l lil \"How can I implement a sorting algorithm?\" 2" + echo -e "\n\tIf number_of_rounds is not provided, the program will default to 2 rounds." + echo -e "\n\t-f <file_path> (optional): Append the contents of the file to the prompt." + echo -e "\n\t-l <language> (optional): Specify programming language focus (default: lil)." + echo -e "\n" + exit 1 +fi + +# --- Argument Parsing --- +FILE_PATH="" +LANGUAGE="lil" +while getopts "f:l:" opt; do + case $opt in + f) + FILE_PATH="$OPTARG" + ;; + l) + LANGUAGE="$OPTARG" + ;; + *) + log_error "Invalid option: -$OPTARG" + exit 1 + ;; + esac +done +shift $((OPTIND -1)) + +PROMPT="$1" +if [ -z "$2" ]; then + ROUNDS=2 +else + ROUNDS=$2 +fi + +# Validate prompt +PROMPT=$(validate_prompt "$PROMPT") +if [ $? -ne 0 ]; then + exit 1 +fi + +# Validate file path if provided +if [ -n "$FILE_PATH" ]; then + if ! validate_file_path "$FILE_PATH"; then + exit 1 + fi + FILE_CONTENTS=$(cat "$FILE_PATH") + PROMPT="$PROMPT\n[FILE CONTENTS]\n$FILE_CONTENTS\n[END FILE]" +fi + +# Validate rounds +if ! [[ "$ROUNDS" =~ ^[1-9][0-9]*$ ]] || [ "$ROUNDS" -gt 5 ]; then + log_error "Invalid number of rounds: $ROUNDS (must be 1-5)" + exit 1 +fi + +# --- File Initialization --- +mkdir -p ~/tmp +SESSION_FILE=~/tmp/puzzle_$(date +%Y%m%d_%H%M%S).txt + +# Initialize timing +SESSION_ID=$(generate_session_id) +start_timer "$SESSION_ID" "puzzle" + +echo "Puzzle Session Log: ${SESSION_FILE}" +echo "---------------------------------" + +# Store the initial user prompt in the session file +echo "USER PROMPT: ${PROMPT}" >> "${SESSION_FILE}" +echo "LANGUAGE FOCUS: ${LANGUAGE}" >> "${SESSION_FILE}" +echo "NUMBER OF ROUNDS: ${ROUNDS}" >> "${SESSION_FILE}" +echo "" >> "${SESSION_FILE}" + +# --- Phase 1: Problem Analysis --- +echo "Phase 1: Analyzing the puzzle or coding challenge..." +echo "PHASE 1 - PROBLEM ANALYSIS:" >> "${SESSION_FILE}" + +# Check for RAG context +RAG_CONTEXT=$(use_rag_if_available "${PROMPT}" "${MECHANISM_NAME}") + +PROBLEM_ANALYSIS_PROMPT="You are an expert puzzle solver and programming mentor specializing in the ${LANGUAGE} programming language. Analyze the following puzzle or coding challenge. + +CHALLENGE: ${PROMPT} + +$(if [[ "$LANGUAGE" == "lil" ]]; then +echo "LIL PROGRAMMING LANGUAGE KNOWLEDGE: +$(get_lil_knowledge) + +Use this comprehensive knowledge of Lil to provide accurate, helpful analysis." +fi) + +$(if [[ "$RAG_CONTEXT" != "$PROMPT" ]]; then +echo "ADDITIONAL CONTEXT FROM KNOWLEDGE BASE: +$RAG_CONTEXT + +Use this context to enhance your analysis if it's relevant to the challenge." +fi) + +Please provide a comprehensive analysis including: +1. Problem type classification (algorithm, data structure, logic, etc.) +2. Complexity assessment (time/space requirements) +3. Key concepts and patterns involved +4. Relevant ${LANGUAGE} language features that could help +5. Potential solution approaches +6. Common pitfalls or edge cases to consider + +Provide a clear, structured analysis that helps understand the problem." + +problem_analysis=$(ollama run "${PUZZLE_MODEL}" "${PROBLEM_ANALYSIS_PROMPT}") +problem_analysis=$(guard_output_quality "$problem_analysis" "$PROMPT" "$MECHANISM_NAME" "$PUZZLE_MODEL") + + + +echo "PROBLEM ANALYSIS:" >> "${SESSION_FILE}" +echo "${problem_analysis}" >> "${SESSION_FILE}" +echo "" >> "${SESSION_FILE}" + +# --- Phase 2: Solution Strategy --- +echo "Phase 2: Developing solution strategies..." +echo "PHASE 2 - SOLUTION STRATEGY:" >> "${SESSION_FILE}" + +SOLUTION_STRATEGY_PROMPT="Based on the problem analysis, develop multiple solution strategies for this puzzle or coding challenge. + +ORIGINAL CHALLENGE: ${PROMPT} + +PROBLEM ANALYSIS: ${problem_analysis} + +$(if [[ "$LANGUAGE" == "lil" ]]; then +echo "LIL PROGRAMMING LANGUAGE KNOWLEDGE: +$(get_lil_knowledge) + +Use this comprehensive knowledge of Lil to provide accurate, helpful analysis." +fi) + +$(if [[ "$RAG_CONTEXT" != "$PROMPT" ]]; then +echo "ADDITIONAL CONTEXT FROM KNOWLEDGE BASE: +$RAG_CONTEXT + +Use this context to develop more informed and accurate solution strategies." +fi) + +Please provide: +1. At least 2-3 different solution approaches +2. Algorithmic complexity analysis for each approach +3. Trade-offs between different solutions +4. Specific ${LANGUAGE} language constructs that would be useful +5. Implementation considerations and challenges +6. Testing and validation strategies + +Focus on practical, implementable solutions with clear reasoning." + +solution_strategy=$(ollama run "${PUZZLE_MODEL}" "${SOLUTION_STRATEGY_PROMPT}") +solution_strategy=$(guard_output_quality "$solution_strategy" "$PROMPT" "$MECHANISM_NAME" "$PUZZLE_MODEL") + + + +echo "SOLUTION STRATEGY:" >> "${SESSION_FILE}" +echo "${solution_strategy}" >> "${SESSION_FILE}" +echo "" >> "${SESSION_FILE}" + +# --- Phase 3: Implementation Guidance --- +echo "Phase 3: Providing implementation guidance..." +echo "PHASE 3 - IMPLEMENTATION GUIDANCE:" >> "${SESSION_FILE}" + +IMPLEMENTATION_PROMPT="Provide detailed implementation guidance for the best solution approach to this puzzle or coding challenge. + +ORIGINAL CHALLENGE: ${PROMPT} +PROBLEM ANALYSIS: ${problem_analysis} +SOLUTION STRATEGY: ${solution_strategy} + +$(if [[ "$LANGUAGE" == "lil" ]]; then +echo "LIL PROGRAMMING LANGUAGE KNOWLEDGE: +$(get_lil_knowledge) + +Use this comprehensive knowledge of Lil to provide accurate, helpful analysis." +fi) + +$(if [[ "$RAG_CONTEXT" != "$PROMPT" ]]; then +echo "ADDITIONAL CONTEXT FROM KNOWLEDGE BASE: +$RAG_CONTEXT + +Use this context to provide more accurate and comprehensive implementation guidance." +fi) + +Please provide: +1. Step-by-step implementation plan +2. Complete code example in ${LANGUAGE} (if applicable) +3. Explanation of key code sections and patterns +4. Variable naming and structure recommendations +5. Error handling and edge case considerations +6. Performance optimization tips +7. Testing and debugging guidance + +Make the implementation clear and educational, explaining the reasoning behind each decision." + +implementation_guidance=$(ollama run "${PUZZLE_MODEL}" "${IMPLEMENTATION_PROMPT}") +implementation_guidance=$(guard_output_quality "$implementation_guidance" "$PROMPT" "$MECHANISM_NAME" "$PUZZLE_MODEL") + + + +echo "IMPLEMENTATION GUIDANCE:" >> "${SESSION_FILE}" +echo "${implementation_guidance}" >> "${SESSION_FILE}" +echo "" >> "${SESSION_FILE}" + +# --- Phase 4: Code Testing (if applicable) --- +if [[ "$LANGUAGE" == "lil" ]] && [[ "$implementation_guidance" =~ (on [a-zA-Z_][a-zA-Z0-9_]* do|function|procedure) ]]; then + echo "Phase 4: Testing the Lil code implementation..." + echo "PHASE 4 - CODE TESTING:" >> "${SESSION_FILE}" + + # Simple code extraction - look for function definitions + lil_code="" + if echo "$implementation_guidance" | grep -q "on [a-zA-Z_][a-zA-Z0-9_]* do"; then + lil_code=$(echo "$implementation_guidance" | grep -A 20 "on [a-zA-Z_][a-zA-Z0-9_]* do" | head -20) + fi + + if [ -n "$lil_code" ]; then + echo "Extracted Lil code for testing:" + echo "----------------------------------------" + echo "$lil_code" + echo "----------------------------------------" + + # Test the extracted code + test_result=$(test_lil_script "$lil_code" "puzzle_implementation") + test_exit_code=$? + + echo "CODE TESTING RESULTS:" >> "${SESSION_FILE}" + echo "$test_result" >> "${SESSION_FILE}" + echo "" >> "${SESSION_FILE}" + + if [ $test_exit_code -eq 0 ]; then + echo "✅ Lil code testing PASSED" + else + echo "❌ Lil code testing FAILED" + echo "Note: The code may have syntax errors or runtime issues." + fi + else + echo "No executable Lil code found in implementation guidance." + echo "CODE TESTING: No executable code found" >> "${SESSION_FILE}" + fi +else + echo "Phase 4: Skipping code testing (not Lil language or no executable code)" + echo "CODE TESTING: Skipped (not applicable)" >> "${SESSION_FILE}" +fi + +# --- Phase 5: Solution Validation --- +echo "Phase 5: Validating and reviewing the solution..." +echo "PHASE 5 - SOLUTION VALIDATION:" >> "${SESSION_FILE}" + +VALIDATION_PROMPT="Review and validate the proposed solution to ensure it's correct, efficient, and well-implemented. + +ORIGINAL CHALLENGE: ${PROMPT} +PROBLEM ANALYSIS: ${problem_analysis} +SOLUTION STRATEGY: ${solution_strategy} +IMPLEMENTATION: ${implementation_guidance} + +Please provide: +1. Code review and correctness verification +2. Edge case analysis and testing scenarios +3. Performance analysis and optimization opportunities +4. Alternative approaches or improvements +5. Common mistakes to avoid +6. Learning resources and next steps +7. Final recommendations and best practices + +Ensure the solution is robust, maintainable, and follows ${LANGUAGE} best practices." + +solution_validation=$(ollama run "${ANALYSIS_MODEL}" "${VALIDATION_PROMPT}") +solution_validation=$(guard_output_quality "$solution_validation" "$PROMPT" "$MECHANISM_NAME" "$ANALYSIS_MODEL") + + + +echo "SOLUTION VALIDATION:" >> "${SESSION_FILE}" +echo "${solution_validation}" >> "${SESSION_FILE}" + +# End timing +duration=$(end_timer "$SESSION_ID" "puzzle") + +# --- Final Output --- +echo "---------------------------------" +echo "Puzzle-solving process complete." +echo "---------------------------------" +echo "" +echo "PROBLEM ANALYSIS:" +echo "=================" +echo "${problem_analysis}" +echo "" +echo "SOLUTION STRATEGY:" +echo "==================" +echo "${solution_strategy}" +echo "" +echo "IMPLEMENTATION GUIDANCE:" +echo "========================" +echo "${implementation_guidance}" +echo "" +if [[ "$LANGUAGE" == "lil" ]] && [[ "$implementation_guidance" =~ (on [a-zA-Z_][a-zA-Z0-9_]* do|function|procedure) ]]; then + echo "CODE TESTING:" + echo "=============" + if [ -n "$lil_code" ]; then + echo "✅ Lil code was tested successfully" + echo "Test results logged in session file" + else + echo "No executable code found for testing" + fi + echo "" +fi +echo "SOLUTION VALIDATION:" +echo "====================" +echo "${solution_validation}" +echo "" +echo "Language focus: ${LANGUAGE}" +echo "Rounds completed: ${ROUNDS}" +echo "Execution time: ${duration} seconds" +echo "" +echo "Full puzzle-solving log: ${SESSION_FILE}" diff --git a/bash/talk-to-computer/quality_guard.sh b/bash/talk-to-computer/quality_guard.sh new file mode 100755 index 0000000..06f8aec --- /dev/null +++ b/bash/talk-to-computer/quality_guard.sh @@ -0,0 +1,366 @@ +#!/bin/bash + +# Quality Guard - System-Wide Output Quality Protection +# This module provides comprehensive quality monitoring for all AI thinking mechanisms +# to prevent output degradation, nonsense, and repetitive responses. + +# --- Configuration --- +MIN_RESPONSE_LENGTH=30 +MAX_REPETITION_RATIO=0.4 +MAX_NONSENSE_SCORE=0.6 +DEGRADATION_THRESHOLD=0.65 +MAX_CORRECTION_ATTEMPTS=2 +FALLBACK_ENABLED=true + +# --- Quality Assessment Functions --- + +# Main quality assessment function +assess_quality() { + local response="$1" + local context="$2" + local mechanism="$3" + + # Calculate quality metrics + local length_score=$(assess_length "$response") + local coherence_score=$(assess_coherence "$response") + local repetition_score=$(assess_repetition "$response") + local relevance_score=$(assess_relevance "$response" "$context" "$mechanism") + local structure_score=$(assess_structure "$response") + + # Weighted quality score + local overall_score=$(echo "scale=2; ($length_score * 0.15 + $coherence_score * 0.25 + $repetition_score * 0.2 + $relevance_score * 0.25 + $structure_score * 0.15)" | bc -l 2>/dev/null || echo "0.5") + + echo "$overall_score" +} + +# Assess response length +assess_length() { + local response="$1" + local word_count=$(echo "$response" | wc -w) + + if [ "$word_count" -lt $MIN_RESPONSE_LENGTH ]; then + echo "0.2" + elif [ "$word_count" -lt 80 ]; then + echo "0.6" + elif [ "$word_count" -lt 200 ]; then + echo "0.9" + elif [ "$word_count" -lt 500 ]; then + echo "0.8" + else + echo "0.7" + fi +} + +# Assess coherence +assess_coherence() { + local response="$1" + + # Check for reasonable sentence structure + local sentences=$(echo "$response" | tr '.' '\n' | grep -v '^[[:space:]]*$' | wc -l) + local avg_length=$(echo "$response" | tr '.' '\n' | grep -v '^[[:space:]]*$' | awk '{sum += length($0)} END {print sum/NR}' 2>/dev/null || echo "50") + + # Penalize extremely long or short sentences + if (( $(echo "$avg_length > 300" | bc -l 2>/dev/null || echo "0") )); then + echo "0.3" + elif (( $(echo "$avg_length < 15" | bc -l 2>/dev/null || echo "0") )); then + echo "0.4" + elif [ "$sentences" -lt 2 ]; then + echo "0.5" + else + echo "0.8" + fi +} + +# Assess repetition +assess_repetition() { + local response="$1" + local unique_words=$(echo "$response" | tr ' ' '\n' | sort | uniq | wc -l) + local total_words=$(echo "$response" | wc -w) + + if [ "$total_words" -eq 0 ]; then + echo "0.0" + else + local repetition_ratio=$(echo "scale=2; $unique_words / $total_words" | bc -l 2>/dev/null || echo "0.5") + + if (( $(echo "$repetition_ratio < $MAX_REPETITION_RATIO" | bc -l 2>/dev/null || echo "0") )); then + echo "0.1" + elif (( $(echo "$repetition_ratio < 0.6" | bc -l 2>/dev/null || echo "0") )); then + echo "0.5" + else + echo "0.9" + fi + fi +} + +# Assess relevance to context and mechanism +assess_relevance() { + local response="$1" + local context="$2" + local mechanism="$3" + + # Mechanism-specific relevance checks + case "$mechanism" in + "puzzle") + if echo "$response" | grep -q -i "algorithm\|code\|implement\|function\|solution"; then + echo "0.9" + else + echo "0.6" + fi + ;; + "socratic") + if echo "$response" | grep -q -i "question\|analyze\|investigate\|examine\|why\|how"; then + echo "0.9" + else + echo "0.6" + fi + ;; + "exploration") + if echo "$response" | grep -q -i "compare\|alternative\|option\|approach\|strategy"; then + echo "0.9" + else + echo "0.6" + fi + ;; + "consensus") + if echo "$response" | grep -q -i "perspective\|view\|opinion\|agree\|disagree\|multiple"; then + echo "0.9" + else + echo "0.6" + fi + ;; + "critique") + if echo "$response" | grep -q -i "improve\|enhance\|fix\|refine\|better\|optimize"; then + echo "0.9" + else + echo "0.6" + fi + ;; + "synthesis") + if echo "$response" | grep -q -i "combine\|integrate\|merge\|unify\|synthesize"; then + echo "0.9" + else + echo "0.6" + fi + ;; + "peer_review") + if echo "$response" | grep -q -i "review\|feedback\|suggest\|advice\|collaborate"; then + echo "0.9" + else + echo "0.6" + fi + ;; + *) + echo "0.7" + ;; + esac +} + +# Assess structural quality +assess_structure() { + local response="$1" + + # Check for proper formatting and structure + local has_paragraphs=$(echo "$response" | grep -c '^[[:space:]]*$' 2>/dev/null | tr -d '[:space:]' || echo "0") + local has_lists=$(echo "$response" | grep -c '^[[:space:]]*[-]' 2>/dev/null | tr -d '[:space:]' || echo "0") + local has_numbers=$(echo "$response" | grep -c '^[[:space:]]*[0-9]' 2>/dev/null | tr -d '[:space:]' || echo "0") + + local structure_score=0.5 + + if [ "${has_paragraphs:-0}" -gt 0 ]; then structure_score=$(echo "$structure_score + 0.2" | bc -l 2>/dev/null || echo "$structure_score"); fi + if [ "${has_lists:-0}" -gt 0 ]; then structure_score=$(echo "$structure_score + 0.15" | bc -l 2>/dev/null || echo "$structure_score"); fi + if [ "${has_numbers:-0}" -gt 0 ]; then structure_score=$(echo "$structure_score + 0.15" | bc -l 2>/dev/null || echo "$structure_score"); fi + + echo "$structure_score" +} + +# --- Degradation Detection --- + +# Detect various degradation patterns +detect_degradation_patterns() { + local response="$1" + local degradation_score=0 + + # Check for nonsense patterns + if echo "$response" | grep -q -i "lorem ipsum\|asdf\|qwerty\|random text\|test test test"; then + degradation_score=$(echo "$degradation_score + 0.9" | bc -l 2>/dev/null || echo "$degradation_score") + fi + + # Check for excessive repetition (simplified pattern) + if echo "$response" | grep -q "the same phrase repeated multiple times"; then + degradation_score=$(echo "$degradation_score + 0.8" | bc -l 2>/dev/null || echo "$degradation_score") + fi + + # Check for incoherent punctuation (more specific - lines with only punctuation) + local punct_only_lines=$(echo "$response" | grep "^[[:space:]]*[[:punct:]]*[[:space:]]*$" | wc -l) + local total_lines=$(echo "$response" | wc -l) + if [ "$total_lines" -gt 0 ]; then + local punct_ratio=$(( punct_only_lines * 100 / total_lines )) + if [ "$punct_ratio" -gt 50 ]; then + # Only flag if more than half the lines are punctuation-only + degradation_score=$(echo "$degradation_score + 0.4" | bc -l 2>/dev/null || echo "$degradation_score") + fi + fi + + # Check for extremely short responses + local word_count=$(echo "$response" | wc -w) + if [ "$word_count" -lt 15 ]; then + degradation_score=$(echo "$degradation_score + 0.5" | bc -l 2>/dev/null || echo "$degradation_score") + fi + + # Check for gibberish (simplified pattern) + if echo "$response" | grep -q "aaaaa\|bbbbb\|ccccc\|ddddd\|eeeee"; then + degradation_score=$(echo "$degradation_score + 0.6" | bc -l 2>/dev/null || echo "$degradation_score") + fi + + # Note: Removed problematic markdown check to eliminate syntax warnings + + echo "$degradation_score" +} + +# --- Correction Mechanisms --- + +# Attempt to correct degraded output +correct_degraded_output() { + local degraded_response="$1" + local context="$2" + local mechanism="$3" + local model="$4" + local attempt=1 + + while [ "$attempt" -le "$MAX_CORRECTION_ATTEMPTS" ]; do + echo "🔄 Correction attempt $attempt/$MAX_CORRECTION_ATTEMPTS..." >&2 + + # Create correction prompt + local correction_prompt="The previous response was degraded or nonsensical. Please provide a clear, coherent response to: + +ORIGINAL REQUEST: $context + +RESPONSE TYPE: $mechanism + +Please ensure your response is: +- Relevant and focused on the request +- Well-structured with proper paragraphs and formatting +- Free of repetition, nonsense, or gibberish +- Appropriate length (at least 50 words) +- Clear and understandable + +Provide a fresh, high-quality response:" + + # Get corrected response + local corrected_response=$(ollama run "$model" "$correction_prompt") + + # Assess correction quality + local correction_quality=$(assess_quality "$corrected_response" "$context" "$mechanism") + local degradation_score=$(detect_degradation_patterns "$corrected_response") + + echo "Correction quality: $correction_quality, Degradation: $degradation_score" >&2 + + # Check if correction is successful + if (( $(echo "$correction_quality > $DEGRADATION_THRESHOLD" | bc -l 2>/dev/null || echo "0") )) && \ + (( $(echo "$degradation_score < $MAX_NONSENSE_SCORE" | bc -l 2>/dev/null || echo "0") )); then + + echo "✅ Output corrected successfully (quality: $correction_quality)" >&2 + echo "$corrected_response" + return 0 + fi + + attempt=$((attempt + 1)) + done + + echo "❌ All correction attempts failed. Using fallback response." >&2 + echo "$(generate_fallback_response "$mechanism" "$context")" + return 1 +} + +# Generate appropriate fallback response +generate_fallback_response() { + local mechanism="$1" + local context="$2" + + case "$mechanism" in + "puzzle") + echo "I apologize, but I'm experiencing difficulties providing a proper response to your puzzle or coding challenge. Please try rephrasing your question or ask for a different type of assistance. You might also want to try breaking down your request into smaller, more specific questions." + ;; + "socratic") + echo "I'm unable to provide the deep analysis you're looking for at this time. Please try asking your question again with more specific details, or consider rephrasing it in a different way." + ;; + "exploration") + echo "I'm having trouble exploring alternatives and strategies for your request. Please try asking your question again or provide more context about what you're looking to explore." + ;; + "consensus") + echo "I cannot provide multiple perspectives or consensus-building guidance currently. Please try rephrasing your request or ask for a different type of assistance." + ;; + "critique") + echo "I'm unable to provide improvement suggestions or critique at this time. Please try asking your question again or request a different approach." + ;; + "synthesis") + echo "I cannot synthesize or combine approaches currently. Please try rephrasing your request or ask for a different form of assistance." + ;; + "peer_review") + echo "I'm having trouble providing collaborative feedback or review. Please try asking your question again or request a different type of help." + ;; + *) + echo "I'm experiencing difficulties providing a proper response. Please try rephrasing your question or ask for a different type of assistance." + ;; + esac +} + +# --- Main Quality Guard Function --- + +# Main function to guard against output degradation +guard_output_quality() { + local response="$1" + local context="$2" + local mechanism="$3" + local model="$4" + + # Assess quality + local quality_score=$(assess_quality "$response" "$context" "$mechanism") + local degradation_score=$(detect_degradation_patterns "$response") + + echo "Quality Score: $quality_score, Degradation Score: $degradation_score" >&2 + + # Check if correction is needed + if (( $(echo "$quality_score < $DEGRADATION_THRESHOLD" | bc -l 2>/dev/null || echo "0") )) || \ + (( $(echo "$degradation_score > $MAX_NONSENSE_SCORE" | bc -l 2>/dev/null || echo "0") )); then + + echo "⚠️ Output quality below threshold. Initiating correction..." >&2 + + if [ "$FALLBACK_ENABLED" = "true" ]; then + correct_degraded_output "$response" "$context" "$mechanism" "$model" + else + echo "❌ Quality below threshold but fallback disabled." >&2 + echo "$response" + fi + else + echo "✅ Response quality acceptable (score: $quality_score)" >&2 + echo "$response" + fi +} + +# --- Utility Functions --- + +# Get mechanism name from script path +get_mechanism_name() { + local script_path="$1" + local script_name=$(basename "$script_path") + + case "$script_name" in + "puzzle") echo "puzzle" ;; + "socratic") echo "socratic" ;; + "exploration") echo "exploration" ;; + "consensus") echo "consensus" ;; + "critique") echo "critique" ;; + "synthesis") echo "synthesis" ;; + "peer-review") echo "peer_review" ;; + *) echo "unknown" ;; + esac +} + +# Export functions for use by other scripts +export -f assess_quality +export -f detect_degradation_patterns +export -f guard_output_quality +export -f get_mechanism_name +export -f correct_degraded_output +export -f generate_fallback_response diff --git a/bash/talk-to-computer/rag_config.sh b/bash/talk-to-computer/rag_config.sh new file mode 100644 index 0000000..27b724b --- /dev/null +++ b/bash/talk-to-computer/rag_config.sh @@ -0,0 +1,118 @@ +#!/bin/bash + +# RAG (Retrieval-Augmented Generation) Configuration +# This file configures the RAG system for corpus-based knowledge augmentation + +# --- Corpus Configuration --- +CORPUS_DIR="corpus" +CORPUS_REGISTRY="${CORPUS_DIR}/corpus_registry.txt" +CORPUS_CACHE_FILE="${CORPUS_DIR}/.corpus_cache" +CORPUS_CACHE_TTL=3600 # Cache TTL in seconds (1 hour) + +# --- Search Configuration --- +MAX_SEARCH_RESULTS=5 +MIN_CONTENT_LENGTH=50 +MAX_CONTENT_LENGTH=5000 +SEARCH_CONTEXT_LINES=3 # Lines of context around search matches + +# --- Topic Classification --- +# Keywords that trigger specific topic matching (format: topic|keywords) +TOPIC_KEYWORDS_FILE="${CORPUS_DIR}/.topic_keywords" + +# Initialize topic keywords file if it doesn't exist +if [ ! -f "$TOPIC_KEYWORDS_FILE" ]; then + cat > "$TOPIC_KEYWORDS_FILE" << 'EOF' +programming|bash shell scripting code algorithm programming software development +lil|decker lil language terse programming scripting deck +science|physics chemistry biology research scientific experiment +physics|quantum relativity mechanics thermodynamics energy force +literature|book author writing novel poem analysis criticism +general|knowledge fact information general misc miscellaneous +EOF +fi + +# --- File Processing --- +# Supported file extensions and their processing commands (format: ext|command) +FILE_PROCESSORS_FILE="${CORPUS_DIR}/.file_processors" + +# Initialize file processors if it doesn't exist +if [ ! -f "$FILE_PROCESSORS_FILE" ]; then + cat > "$FILE_PROCESSORS_FILE" << 'EOF' +txt|cat +md|cat +html|cat +EOF +fi + +# --- Search Tools --- +# Commands used for searching different file types +GREP_CMD="grep -r -i --include=\"*.txt\" --include=\"*.md\" --include=\"*.html\"" +SED_CMD="sed" +AWK_CMD="awk" + +# --- RAG Behavior --- +RAG_ENABLED=true +RAG_CONFIDENCE_THRESHOLD=0.7 # Minimum confidence to trigger RAG +RAG_MAX_CONTEXT_LENGTH=4000 # Maximum context to include in prompt +RAG_CACHE_ENABLED=true + +# --- Debug and Logging --- +RAG_DEBUG=false +RAG_LOG_FILE="logs/rag_system.log" + +# --- Utility Functions --- + +# Check if RAG system is properly configured +check_rag_system() { + local issues=() + + # Check if corpus directory exists + if [ ! -d "$CORPUS_DIR" ]; then + issues+=("Corpus directory not found: $CORPUS_DIR") + fi + + # Check if registry exists + if [ ! -f "$CORPUS_REGISTRY" ]; then + issues+=("Corpus registry not found: $CORPUS_REGISTRY") + fi + + # Check if corpus manager exists + if [ ! -f "corpus_manager.sh" ]; then + issues+=("Corpus manager not found: corpus_manager.sh") + fi + + # Report issues + if [ ${#issues[@]} -gt 0 ]; then + echo "❌ RAG System Issues Found:" + for issue in "${issues[@]}"; do + echo " - $issue" + done + return 1 + else + echo "✅ RAG System is properly configured" + return 0 + fi +} + +# Get corpus statistics +get_corpus_stats() { + if [ -f "$CORPUS_REGISTRY" ]; then + local topic_count=$(grep -c "|" "$CORPUS_REGISTRY") + local file_count=$(find "$CORPUS_DIR" -type f \( -name "*.txt" -o -name "*.md" -o -name "*.html" \) 2>/dev/null | wc -l) + echo "📊 Corpus Statistics:" + echo " Topics: $topic_count" + echo " Files: $file_count" + else + echo "❌ No corpus registry found" + fi +} + +# Export configuration for use by other scripts +export CORPUS_DIR CORPUS_REGISTRY CORPUS_CACHE_FILE CORPUS_CACHE_TTL +export MAX_SEARCH_RESULTS MIN_CONTENT_LENGTH MAX_CONTENT_LENGTH SEARCH_CONTEXT_LINES +export RAG_ENABLED RAG_CONFIDENCE_THRESHOLD RAG_MAX_CONTEXT_LENGTH RAG_CACHE_ENABLED +export RAG_DEBUG RAG_LOG_FILE +export GREP_CMD SED_CMD AWK_CMD + +# Make utility functions available +export -f check_rag_system get_corpus_stats diff --git a/bash/talk-to-computer/rag_integration.sh b/bash/talk-to-computer/rag_integration.sh new file mode 100644 index 0000000..6c974df --- /dev/null +++ b/bash/talk-to-computer/rag_integration.sh @@ -0,0 +1,336 @@ +#!/bin/bash + +# RAG Integration Module +# This module provides functions for thinking mechanisms to intelligently query the RAG corpus +# and integrate relevant context into their prompts + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/rag_config.sh" + +# --- RAG Query Functions --- + +# Main function for mechanisms to query RAG system +query_rag_context() { + local prompt="$1" + local mechanism="$2" + local max_context="${3:-$RAG_MAX_CONTEXT_LENGTH}" + + # Determine if RAG should be used + if should_use_rag "$prompt" "$mechanism"; then + local corpus_results + corpus_results=$(get_relevant_context "$prompt" "$mechanism" "$max_context") + + if [ -n "$corpus_results" ]; then + echo "RAG_CONTEXT_AVAILABLE: $corpus_results" + return 0 + fi + fi + + echo "RAG_CONTEXT_AVAILABLE: NONE" + return 1 +} + +# Determine if RAG should be used for this prompt/mechanism combination +should_use_rag() { + local prompt="$1" + local mechanism="$2" + + # Skip RAG if disabled + if [ "$RAG_ENABLED" != "true" ]; then + return 1 + fi + + # Check mechanism-specific RAG usage + case "$mechanism" in + "puzzle") + # Always use RAG for puzzle mechanism (coding/programming) + return 0 + ;; + "socratic") + # Use RAG for technical or complex topics + if echo "$prompt" | grep -q -i '\(technical\|complex\|advanced\|algorithm\|programming\|science\)'; then + return 0 + fi + ;; + "exploration") + # Use RAG for specific technical domains + if echo "$prompt" | grep -q -i '\(technology\|framework\|methodology\|architecture\)'; then + return 0 + fi + ;; + "critique") + # Use RAG for domain-specific improvement requests + if echo "$prompt" | grep -q -i '\(improve\|optimize\|enhance\|refactor\)'; then + return 0 + fi + ;; + esac + + # Default: don't use RAG unless explicitly triggered + return 1 +} + +# Get relevant context from corpus for the given prompt and mechanism +get_relevant_context() { + local prompt="$1" + local mechanism="$2" + local max_context="$3" + + # Extract key search terms from prompt + local search_terms + search_terms=$(extract_search_terms "$prompt" "$mechanism") + + if [ -z "$search_terms" ]; then + return 1 + fi + + local context="" + + # Try each search term + for term in $search_terms; do + local corpus_path + corpus_path=$(find_relevant_corpus "$term" "$mechanism") + + if [ -n "$corpus_path" ] && [ -d "$corpus_path" ]; then + local term_context + term_context=$(search_corpus_term "$term" "$corpus_path" "$max_context") + + if [ -n "$term_context" ]; then + context="${context}\n=== Context for '$term' ===\n${term_context}\n" + fi + fi + done + + # Trim context if too long + if [ ${#context} -gt "$max_context" ]; then + context=$(echo "$context" | head -c "$max_context") + context="${context}...\n[Content truncated for length]" + fi + + echo "$context" +} + +# Extract search terms from prompt based on mechanism +extract_search_terms() { + local prompt="$1" + local mechanism="$2" + + case "$mechanism" in + "puzzle") + # Extract programming-related terms + echo "$prompt" | grep -o -i '\b\(algorithm\|function\|variable\|class\|method\|programming\|code\|implement\|solve\)\w*' | head -5 + ;; + "socratic") + # Extract technical concepts + echo "$prompt" | grep -o -i '\b\(concept\|principle\|theory\|approach\|methodology\|framework\)\w*' | head -3 + ;; + "exploration") + # Extract comparison terms + echo "$prompt" | grep -o -i '\b\(compare\|versus\|alternative\|option\|approach\|strategy\)\w*' | head -3 + ;; + "critique") + # Extract improvement terms + echo "$prompt" | grep -o -i '\b\(improve\|optimize\|enhance\|fix\|refactor\|performance\|quality\)\w*' | head -3 + ;; + *) + # Generic term extraction + echo "$prompt" | grep -o -i '\b\w\{5,\}\b' | head -3 + ;; + esac +} + +# Find relevant corpus directory for a search term +find_relevant_corpus() { + local search_term="$1" + local mechanism="$2" + + # Try mechanism-specific corpus mapping first + case "$mechanism" in + "puzzle") + if echo "$search_term" | grep -q -i '\(lil\|programming\|algorithm\)'; then + echo "$CORPUS_DIR/programming" + return 0 + fi + ;; + "socratic") + if echo "$search_term" | grep -q -i '\(science\|physics\|chemistry\|biology\)'; then + echo "$CORPUS_DIR/science" + return 0 + fi + ;; + esac + + # Try to find corpus based on term + if echo "$search_term" | grep -q -i '\(programming\|code\|algorithm\|function\)'; then + echo "$CORPUS_DIR/programming" + elif echo "$search_term" | grep -q -i '\(science\|physics\|chemistry\|biology\)'; then + echo "$CORPUS_DIR/science" + elif echo "$search_term" | grep -q -i '\(literature\|book\|author\|writing\)'; then + echo "$CORPUS_DIR/literature" + else + # Default to general corpus + echo "$CORPUS_DIR/general" + fi +} + +# Search corpus for a specific term and return relevant content +search_corpus_term() { + local search_term="$1" + local corpus_path="$2" + local max_context="$3" + + # Use grep to find relevant content + local results + results=$(grep -r -i -A 5 -B 2 "$search_term" "$corpus_path" --include="*.txt" --include="*.md" --include="*.html" 2>/dev/null | head -20) + + if [ -n "$results" ]; then + echo "$results" + return 0 + fi + + return 1 +} + +# --- Context Integration Functions --- + +# Integrate RAG context into a prompt +integrate_rag_context() { + local original_prompt="$1" + local rag_context="$2" + local mechanism="$3" + + if [ "$rag_context" = "RAG_CONTEXT_AVAILABLE: NONE" ] || [ -z "$rag_context" ]; then + echo "$original_prompt" + return 0 + fi + + # Extract actual context content + local context_content + context_content=$(echo "$rag_context" | sed 's/^RAG_CONTEXT_AVAILABLE: //') + + # Create context-aware prompt based on mechanism + case "$mechanism" in + "puzzle") + cat << EOF +I have access to relevant programming knowledge that may help answer this question: + +$context_content + +Original Question: $original_prompt + +Please use the above context to provide a more accurate and helpful response. If the context is relevant, incorporate it naturally into your answer. If it's not directly relevant, you can ignore it and answer based on your general knowledge. +EOF + ;; + "socratic") + cat << EOF +Relevant context from knowledge base: + +$context_content + +Question for analysis: $original_prompt + +Consider the above context when formulating your response. Use it to provide deeper insights and more accurate analysis if relevant. +EOF + ;; + "exploration") + cat << EOF +Additional context that may be relevant: + +$context_content + +Exploration topic: $original_prompt + +Use the provided context to enrich your analysis and provide more comprehensive alternatives if applicable. +EOF + ;; + *) + cat << EOF +Context from knowledge base: + +$context_content + +$original_prompt + +You may use the above context to enhance your response if it's relevant to the question. +EOF + ;; + esac +} + +# --- Utility Functions --- + +# Check if corpus is available and functional +check_corpus_health() { + local issues=() + + # Check if corpus directory exists + if [ ! -d "$CORPUS_DIR" ]; then + issues+=("Corpus directory not found: $CORPUS_DIR") + fi + + # Check if registry exists + if [ ! -f "$CORPUS_REGISTRY" ]; then + issues+=("Corpus registry not found: $CORPUS_REGISTRY") + fi + + # Check if registry has content + if [ -f "$CORPUS_REGISTRY" ] && [ $(wc -l < "$CORPUS_REGISTRY") -le 3 ]; then + issues+=("Corpus registry appears to be empty") + fi + + # Report issues + if [ ${#issues[@]} -gt 0 ]; then + echo "❌ RAG Integration Issues Found:" + for issue in "${issues[@]}"; do + echo " - $issue" + done + return 1 + else + echo "✅ RAG Integration is healthy" + return 0 + fi +} + +# Get RAG statistics +get_rag_stats() { + if [ ! -f "$CORPUS_REGISTRY" ]; then + echo "❌ No corpus registry found" + return 1 + fi + + local topic_count=$(grep -c "|" "$CORPUS_REGISTRY") + local file_count=$(find "$CORPUS_DIR" -type f \( -name "*.txt" -o -name "*.md" -o -name "*.html" \) 2>/dev/null | wc -l) + + echo "📊 RAG System Statistics:" + echo " Topics: $topic_count" + echo " Files: $file_count" + echo " Status: $(if [ "$RAG_ENABLED" = "true" ]; then echo "Enabled"; else echo "Disabled"; fi)" + echo " Max Context: $RAG_MAX_CONTEXT_LENGTH characters" +} + +# --- Integration Helper --- + +# Helper function for mechanisms to easily use RAG +use_rag_if_available() { + local prompt="$1" + local mechanism="$2" + + local rag_result + rag_result=$(query_rag_context "$prompt" "$mechanism") + + if echo "$rag_result" | grep -q "^RAG_CONTEXT_AVAILABLE: " && ! echo "$rag_result" | grep -q "NONE$"; then + echo "RAG context found - integrating into prompt" + integrate_rag_context "$prompt" "$rag_result" "$mechanism" + return 0 + else + echo "No RAG context available - using original prompt" + echo "$prompt" + return 1 + fi +} + +# Export functions for use by other scripts +export -f query_rag_context should_use_rag get_relevant_context +export -f extract_search_terms find_relevant_corpus search_corpus_term +export -f integrate_rag_context check_corpus_health get_rag_stats +export -f use_rag_if_available diff --git a/bash/talk-to-computer/rag_search.sh b/bash/talk-to-computer/rag_search.sh new file mode 100755 index 0000000..dfcbc91 --- /dev/null +++ b/bash/talk-to-computer/rag_search.sh @@ -0,0 +1,187 @@ +#!/bin/bash + +# RAG Search Utility - Search the knowledge corpus +# This script demonstrates how to search the corpus using efficient Unix tools + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/rag_config.sh" + +# --- Utility Functions --- + +# Get corpus path for a topic (standalone version) +get_corpus_path() { + local topic="$1" + if [ -f "$CORPUS_REGISTRY" ]; then + grep "^[^|]*${topic}|" "$CORPUS_REGISTRY" | head -1 | cut -d'|' -f2 + fi +} + +# Check if corpus exists for a topic +corpus_exists() { + local topic="$1" + grep -q "^[^|]*${topic}|" "$CORPUS_REGISTRY" 2>/dev/null +} + +# --- Search Functions --- + +# Search corpus for keywords +search_corpus() { + local query="$1" + local topic="${2:-}" + + echo "🔍 Searching corpus for: '$query'" + if [ -n "$topic" ]; then + echo "📂 Limited to topic: $topic" + fi + echo "----------------------------------------" + + # Build search command + if [ -n "$topic" ]; then + local corpus_path=$(get_corpus_path "$topic") + if [ -n "$corpus_path" ]; then + # Search specific topic directory + grep -r -i "$query" "$corpus_path" --include="*.txt" --include="*.md" --include="*.html" + else + echo "❌ Topic not found: $topic" + return 1 + fi + else + # Search entire corpus + grep -r -i "$query" "$CORPUS_DIR" --include="*.txt" --include="*.md" --include="*.html" + fi | head -10 | while IFS=: read -r file line content; do + local filename=$(basename "$file") + local topic_name=$(basename "$(dirname "$file")") + echo "📄 $topic_name/$filename (line $line):" + echo " $content" + echo "" + done +} + +# Get context around search results +get_context() { + local query="$1" + local topic="$2" + local context_lines="${3:-$SEARCH_CONTEXT_LINES}" + + echo "📖 Getting context for: '$query'" + echo "----------------------------------------" + + if [ -n "$topic" ]; then + local corpus_path=$(get_corpus_path "$topic") + if [ -n "$corpus_path" ]; then + grep -r -i -A "$context_lines" -B "$context_lines" "$query" "$corpus_path" + else + echo "❌ Topic not found: $topic" + return 1 + fi + else + grep -r -i -A "$context_lines" -B "$context_lines" "$query" "$CORPUS_DIR" + fi +} + +# Extract relevant sections from files +extract_sections() { + local query="$1" + local topic="$2" + + echo "📋 Extracting relevant sections for: '$query'" + echo "----------------------------------------" + + # Find files containing the query + local files + if [ -n "$topic" ]; then + local corpus_path=$(get_corpus_path "$topic") + files=$(grep -r -l -i "$query" "$corpus_path" 2>/dev/null) + else + files=$(grep -r -l -i "$query" "$CORPUS_DIR" 2>/dev/null) + fi + + if [ -z "$files" ]; then + echo "❌ No files found containing: $query" + return 1 + fi + + echo "$files" | while read -r file; do + local filename=$(basename "$file") + echo "📄 Processing: $filename" + echo "----------------------------------------" + + # Extract relevant sections (headers and surrounding content) + awk -v query="$query" ' + BEGIN { in_section = 0; section_content = "" } + + # Check if line contains query (case insensitive) + tolower($0) ~ tolower(query) { + if (in_section == 0) { + print "RELEVANT SECTION:" + in_section = 1 + } + } + + # If we found a header before the match, include it + /^#/ && in_section == 0 { + section_content = $0 + } + + # Print content when we have a match + in_section == 1 { + print + if (length($0) == 0) { + in_section = 0 + section_content = "" + print "" + } + } + ' "$file" + + echo "----------------------------------------" + done +} + +# --- Main Command Interface --- + +case "${1:-help}" in + "search") + if [ -n "$2" ]; then + search_corpus "$2" "$3" + else + echo "❌ Usage: $0 search <query> [topic]" + fi + ;; + "context") + if [ -n "$2" ]; then + get_context "$2" "$3" "$4" + else + echo "❌ Usage: $0 context <query> [topic] [lines]" + fi + ;; + "extract") + if [ -n "$2" ]; then + extract_sections "$2" "$3" + else + echo "❌ Usage: $0 extract <query> [topic]" + fi + ;; + "stats") + get_corpus_stats + ;; + "help"|*) + echo "🔍 RAG Search Utility" + echo "Search and extract information from the knowledge corpus" + echo "" + echo "Usage: $0 <command> [arguments]" + echo "" + echo "Commands:" + echo " search <query> [topic] Search for exact matches" + echo " context <query> [topic] Get context around matches" + echo " extract <query> [topic] Extract relevant sections" + echo " stats Show corpus statistics" + echo " help Show this help message" + echo "" + echo "Examples:" + echo " $0 search 'quantum physics'" + echo " $0 search 'lil programming' programming" + echo " $0 context 'force' physics" + echo " $0 extract 'variables' programming" + ;; +esac diff --git a/bash/socratic b/bash/talk-to-computer/socratic index 8da0e3d..a685875 100755 --- a/bash/socratic +++ b/bash/talk-to-computer/socratic @@ -39,12 +39,22 @@ # with each iteration potentially revealing new insights or limitations. # The system emphasizes depth of analysis and intellectual honesty over definitive answers. -# --- Model Configuration --- -RESPONSE_MODEL="llama3:8b-instruct-q4_K_M" -QUESTION_MODEL="phi3:3.8b-mini-4k-instruct-q4_K_M" +# Initialize common functionality +source "$(dirname "${BASH_SOURCE[0]}")/common.sh" +init_thinking_mechanism "${BASH_SOURCE[0]}" -# --- Defaults --- -DEFAULT_ROUNDS=2 +# Use centralized model configuration +RESPONSE_MODEL="$SOCRATIC_RESPONSE_MODEL" +QUESTION_MODEL="$SOCRATIC_QUESTION_MODEL" + +# Validate and set models with standardized error handling +if ! validate_and_set_model "RESPONSE_MODEL" "$RESPONSE_MODEL" "$FALLBACK_MODEL"; then + handle_model_error "$RESPONSE_MODEL" "$(basename "$0")" +fi + +if ! validate_and_set_model "QUESTION_MODEL" "$QUESTION_MODEL" "$FALLBACK_MODEL"; then + handle_model_error "$QUESTION_MODEL" "$(basename "$0")" +fi # --- Argument Validation --- if [ "$#" -lt 1 ]; then @@ -83,10 +93,12 @@ fi # If file path is provided, append its contents to the prompt if [ -n "$FILE_PATH" ]; then if [ ! -f "$FILE_PATH" ]; then - echo "File not found: $FILE_PATH" >&2 - exit 1 + handle_file_error "$FILE_PATH" "find" "$(basename "$0")" + fi + if [ ! -r "$FILE_PATH" ]; then + handle_file_error "$FILE_PATH" "read" "$(basename "$0")" fi - FILE_CONTENTS=$(cat "$FILE_PATH") + FILE_CONTENTS=$(cat "$FILE_PATH" 2>/dev/null) || handle_file_error "$FILE_PATH" "read contents of" "$(basename "$0")" PROMPT="$PROMPT\n[FILE CONTENTS]\n$FILE_CONTENTS\n[END FILE]" fi @@ -114,6 +126,7 @@ INITIAL_PROMPT="You are an expert assistant. Please provide a comprehensive resp PROMPT: ${PROMPT}" INITIAL_RESPONSE=$(ollama run "${RESPONSE_MODEL}" "${INITIAL_PROMPT}") +INITIAL_RESPONSE=$(guard_output_quality "$INITIAL_RESPONSE" "$PROMPT" "$MECHANISM_NAME" "$RESPONSE_MODEL") echo "INITIAL RESPONSE (${RESPONSE_MODEL}):" >> "${SESSION_FILE}" echo "${INITIAL_RESPONSE}" >> "${SESSION_FILE}" @@ -145,6 +158,7 @@ RESPONSE TO QUESTION: ${CURRENT_RESPONSE} Generate your questions in a clear, numbered format. Be specific and avoid yes/no questions." QUESTIONS=$(ollama run "${QUESTION_MODEL}" "${QUESTION_PROMPT}") + QUESTIONS=$(guard_output_quality "$QUESTIONS" "$PROMPT" "$MECHANISM_NAME" "$QUESTION_MODEL") echo "QUESTIONS (${QUESTION_MODEL}):" >> "${SESSION_FILE}" echo "${QUESTIONS}" >> "${SESSION_FILE}" @@ -168,6 +182,7 @@ Please provide a comprehensive response that: - Refines or expands your original response based on the questioning" REFINED_RESPONSE=$(ollama run "${RESPONSE_MODEL}" "${REFINE_PROMPT}") + REFINED_RESPONSE=$(guard_output_quality "$REFINED_RESPONSE" "$PROMPT" "$MECHANISM_NAME" "$RESPONSE_MODEL") echo "REFINED RESPONSE (${RESPONSE_MODEL}):" >> "${SESSION_FILE}" echo "${REFINED_RESPONSE}" >> "${SESSION_FILE}" @@ -198,6 +213,7 @@ Please provide a summary that: - Is clear, concise, and well-organized" FINAL_SUMMARY=$(ollama run "${RESPONSE_MODEL}" "${SUMMARY_PROMPT}") +FINAL_SUMMARY=$(guard_output_quality "$FINAL_SUMMARY" "$PROMPT" "$MECHANISM_NAME" "$RESPONSE_MODEL") echo "FINAL SUMMARY (${RESPONSE_MODEL}):" >> "${SESSION_FILE}" echo "${FINAL_SUMMARY}" >> "${SESSION_FILE}" diff --git a/bash/synthesis b/bash/talk-to-computer/synthesis index 417279e..b91c9b5 100755 --- a/bash/synthesis +++ b/bash/talk-to-computer/synthesis @@ -44,6 +44,12 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Source the logging system using absolute path source "${SCRIPT_DIR}/logging.sh" +# Source the quality guard for output quality protection +source "${SCRIPT_DIR}/quality_guard.sh" + +# Get mechanism name automatically +MECHANISM_NAME=$(get_mechanism_name "$0") + # --- Model Configuration --- SYNTHESIS_MODEL="llama3:8b-instruct-q4_K_M" @@ -178,6 +184,7 @@ Please identify: Provide a clear analysis of conflicts and potential resolutions." conflict_analysis=$(ollama run "${SYNTHESIS_MODEL}" "${CONFLICT_PROMPT}") +conflict_analysis=$(guard_output_quality "$conflict_analysis" "$PROMPT" "$MECHANISM_NAME" "$SYNTHESIS_MODEL") echo "CONFLICT ANALYSIS:" >> "${SESSION_FILE}" echo "${conflict_analysis}" >> "${SESSION_FILE}" @@ -219,6 +226,7 @@ Please create a unified synthesis that: Your synthesis should be greater than the sum of its parts - it should provide insights that individual mechanisms might miss." final_synthesis=$(ollama run "${SYNTHESIS_MODEL}" "${SYNTHESIS_PROMPT}") +final_synthesis=$(guard_output_quality "$final_synthesis" "$PROMPT" "$MECHANISM_NAME" "$SYNTHESIS_MODEL") echo "FINAL SYNTHESIS:" >> "${SESSION_FILE}" echo "${final_synthesis}" >> "${SESSION_FILE}" diff --git a/bash/talk-to-computer/test_framework.sh b/bash/talk-to-computer/test_framework.sh new file mode 100755 index 0000000..c74ad56 --- /dev/null +++ b/bash/talk-to-computer/test_framework.sh @@ -0,0 +1,434 @@ +#!/bin/bash + +# Comprehensive Test Framework for AI Thinking Mechanisms +# This script provides automated testing capabilities for all system components. + +# Source common functionality +source "$(dirname "${BASH_SOURCE[0]}")/common.sh" +source "$(dirname "${BASH_SOURCE[0]}")/config.sh" + +# --- Test Configuration --- + +# Test directories +TEST_DIR="${LOG_DIR}/tests" +RESULTS_DIR="${TEST_DIR}/results" +COVERAGE_DIR="${TEST_DIR}/coverage" + +# Test counters +TESTS_PASSED=0 +TESTS_FAILED=0 +TESTS_SKIPPED=0 + +# --- Test Utilities --- + +# Initialize test framework +init_test_framework() { + mkdir -p "$RESULTS_DIR" "$COVERAGE_DIR" + echo "🧪 AI Thinking Mechanisms Test Framework" + echo "========================================" + echo +} + +# Test result functions +test_pass() { + local test_name="$1" + echo "✅ PASS: $test_name" + ((TESTS_PASSED++)) +} + +test_fail() { + local test_name="$1" + local reason="$2" + echo "❌ FAIL: $test_name - $reason" + ((TESTS_FAILED++)) +} + +test_skip() { + local test_name="$1" + local reason="$2" + echo "⏭️ SKIP: $test_name - $reason" + ((TESTS_SKIPPED++)) +} + +# Assert functions +assert_equals() { + local expected="$1" + local actual="$2" + local test_name="$3" + + if [ "$expected" = "$actual" ]; then + test_pass "$test_name" + else + test_fail "$test_name" "Expected '$expected', got '$actual'" + fi +} + +assert_not_empty() { + local value="$1" + local test_name="$2" + + if [ -n "$value" ]; then + test_pass "$test_name" + else + test_fail "$test_name" "Value is empty" + fi +} + +assert_file_exists() { + local file_path="$1" + local test_name="$2" + + if [ -f "$file_path" ]; then + test_pass "$test_name" + else + test_fail "$test_name" "File does not exist: $file_path" + fi +} + +# --- Component Tests --- + +test_common_functions() { + echo "Testing Common Functions..." + + # Test script directory detection + local script_dir + script_dir=$(get_script_dir) + assert_not_empty "$script_dir" "get_script_dir" + + # Test model validation (if ollama is available) + if command_exists ollama; then + local result + result=$(validate_model "gemma3n:e2b" "gemma3n:e2b") + if [ $? -eq 0 ]; then + test_pass "validate_model_success" + else + test_skip "validate_model_success" "Model not available" + fi + else + test_skip "validate_model_success" "Ollama not available" + fi +} + +test_config_loading() { + echo "Testing Configuration Loading..." + + # Test that config variables are loaded + if [ -n "$DEFAULT_MODEL" ]; then + test_pass "config_default_model" + else + test_fail "config_default_model" "DEFAULT_MODEL not set" + fi + + if [ -n "$FALLBACK_MODEL" ]; then + test_pass "config_fallback_model" + else + test_fail "config_fallback_model" "FALLBACK_MODEL not set" + fi + + # Test model arrays + if [ ${#CONSENSUS_MODELS[@]} -gt 0 ]; then + test_pass "config_consensus_models" + else + test_fail "config_consensus_models" "CONSENSUS_MODELS array is empty" + fi +} + +test_quality_guard() { + echo "Testing Quality Guard..." + + source "./quality_guard.sh" + + # Test quality assessment + local test_response="This is a comprehensive answer that should pass quality checks." + local quality_score + quality_score=$(assess_quality "$test_response" "test prompt" "socratic") + assert_not_empty "$quality_score" "assess_quality" + + # Test degradation detection + local degradation_score + degradation_score=$(detect_degradation_patterns "$test_response") + assert_not_empty "$degradation_score" "detect_degradation_patterns" + + # Test degraded response detection + local lorem_response="Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt" + local lorem_degradation + lorem_degradation=$(detect_degradation_patterns "$lorem_response") + + if (( $(echo "$lorem_degradation > 0" | bc -l 2>/dev/null || echo "0") )); then + test_pass "lorem_ipsum_detection" + else + test_fail "lorem_ipsum_detection" "Failed to detect lorem ipsum pattern" + fi +} + +test_logging_system() { + echo "Testing Logging System..." + + source "./logging.sh" + + # Test error logging + log_error "Test error message" + if [ -f "$ERROR_LOG" ]; then + test_pass "error_logging" + else + test_fail "error_logging" "Error log file not created" + fi + + # Test validation functions + local temp_file + temp_file=$(create_managed_temp_file "test" "tmp") + echo "test content" > "$temp_file" + + if validate_file_path "$temp_file"; then + test_pass "validate_file_path" + else + test_fail "validate_file_path" "Failed to validate existing file" + fi + + # Test invalid file + if ! validate_file_path "/nonexistent/file.txt" 2>/dev/null; then + test_pass "validate_invalid_file" + else + test_fail "validate_invalid_file" "Should have failed for nonexistent file" + fi +} + +test_resource_management() { + echo "Testing Resource Management..." + + source "./common.sh" + + # Test temporary directory creation + local temp_dir + temp_dir=$(create_managed_temp_dir "test") + if [ -d "$temp_dir" ]; then + test_pass "create_temp_dir" + else + test_fail "create_temp_dir" "Failed to create temp directory" + fi + + # Test cleanup registration + register_cleanup_resource "$temp_dir" + if [ ${#CLEANUP_RESOURCES[@]} -gt 0 ]; then + test_pass "register_cleanup_resource" + else + test_fail "register_cleanup_resource" "Resource not registered for cleanup" + fi +} + +# --- Integration Tests --- + +test_mechanism_integration() { + echo "Testing Mechanism Integration..." + + # Test if mechanisms are executable + local mechanisms=("socratic" "exploration" "consensus" "critique" "synthesis" "peer-review" "puzzle") + + for mechanism in "${mechanisms[@]}"; do + if [ -x "./$mechanism" ]; then + test_pass "mechanism_executable_$mechanism" + else + test_fail "mechanism_executable_$mechanism" "Mechanism not executable" + fi + done +} + +test_classifier_integration() { + echo "Testing Classifier Integration..." + + if [ -x "./classifier.sh" ]; then + test_pass "classifier_executable" + + # Test basic classification (if possible without models) + local test_result + test_result=$(source "./classifier.sh" && analyze_intent_patterns "What are the different approaches to solving this problem?" 2>/dev/null) + if [ -n "$test_result" ]; then + test_pass "classifier_basic_functionality" + else + test_skip "classifier_basic_functionality" "Cannot test without models" + fi + else + test_fail "classifier_executable" "Classifier script not executable" + fi +} + +# --- Performance Tests --- + +test_performance_metrics() { + echo "Testing Performance Metrics..." + + source "./logging.sh" + + # Test metrics functions exist + if command -v log_session_start >/dev/null 2>&1; then + test_pass "performance_functions_available" + else + test_fail "performance_functions_available" "Performance logging functions not available" + fi + + # Test metrics file creation + if [ -f "$METRICS_FILE" ] || touch "$METRICS_FILE" 2>/dev/null; then + test_pass "metrics_file_accessible" + else + test_fail "metrics_file_accessible" "Cannot access metrics file" + fi +} + +# --- Main Test Runner --- + +run_all_tests() { + init_test_framework + + echo "Running Test Suite..." + echo "=====================" + echo + + # Unit Tests + test_common_functions + echo + + test_config_loading + echo + + test_quality_guard + echo + + test_logging_system + echo + + test_resource_management + echo + + # Integration Tests + test_mechanism_integration + echo + + test_classifier_integration + echo + + # Performance Tests + test_performance_metrics + echo + + # Test Summary + echo "Test Summary" + echo "============" + echo "✅ Passed: $TESTS_PASSED" + echo "❌ Failed: $TESTS_FAILED" + echo "⏭️ Skipped: $TESTS_SKIPPED" + echo + + local total_tests=$((TESTS_PASSED + TESTS_FAILED)) + if [ $total_tests -gt 0 ]; then + local pass_rate=$((TESTS_PASSED * 100 / total_tests)) + echo "Pass Rate: $pass_rate%" + + if [ $TESTS_FAILED -eq 0 ]; then + echo "🎉 All tests completed successfully!" + return 0 + else + echo "⚠️ Some tests failed. Please review the results above." + return 1 + fi + else + echo "No tests were run." + return 1 + fi +} + +# --- CLI Interface --- + +show_help() { + echo "AI Thinking Mechanisms Test Framework" + echo "Usage: $0 [OPTIONS]" + echo + echo "Options:" + echo " -a, --all Run all tests (default)" + echo " -u, --unit Run only unit tests" + echo " -i, --integration Run only integration tests" + echo " -p, --performance Run only performance tests" + echo " -v, --verbose Enable verbose output" + echo " -h, --help Show this help message" + echo + echo "Examples:" + echo " $0 -a # Run all tests" + echo " $0 -u -v # Run unit tests with verbose output" + echo " $0 -p # Run only performance tests" +} + +# Parse command line arguments +VERBOSE=false +TEST_TYPE="all" + +while [[ $# -gt 0 ]]; do + case $1 in + -a|--all) + TEST_TYPE="all" + shift + ;; + -u|--unit) + TEST_TYPE="unit" + shift + ;; + -i|--integration) + TEST_TYPE="integration" + shift + ;; + -p|--performance) + TEST_TYPE="performance" + shift + ;; + -v|--verbose) + VERBOSE=true + shift + ;; + -h|--help) + show_help + exit 0 + ;; + *) + echo "Unknown option: $1" + show_help + exit 1 + ;; + esac +done + +# Set verbose output +if [ "$VERBOSE" = true ]; then + set -x +fi + +# Run tests based on type +case $TEST_TYPE in + "all") + run_all_tests + ;; + "unit") + init_test_framework + test_common_functions + echo + test_config_loading + echo + test_quality_guard + echo + test_logging_system + echo + test_resource_management + ;; + "integration") + init_test_framework + test_mechanism_integration + echo + test_classifier_integration + ;; + "performance") + init_test_framework + test_performance_metrics + ;; + *) + echo "Invalid test type: $TEST_TYPE" + show_help + exit 1 + ;; +esac diff --git a/bash/talk-to-computer/test_model_selector.sh b/bash/talk-to-computer/test_model_selector.sh new file mode 100755 index 0000000..f727d42 --- /dev/null +++ b/bash/talk-to-computer/test_model_selector.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +# Test script for the Dynamic Model Selector + +source "./model_selector.sh" + +echo "=== Dynamic Model Selector Test ===" +echo + +# Test 1: Show available models +echo "Test 1: Available Models" +echo "Available models:" +get_available_models | nl +echo + +# Test 2: Task type classification +echo "Test 2: Task Type Classification" +echo "Coding task: $(classify_task_type "How can I implement a binary search algorithm?" "puzzle")" +echo "Reasoning task: $(classify_task_type "Why do you think this approach might fail?" "socratic")" +echo "Creative task: $(classify_task_type "Write a story about a robot" "exploration")" +echo + +# Test 3: Model selection for coding task +echo "Test 3: Model Selection for Coding Task" +selected_model=$(select_model_for_task "How can I implement a sorting algorithm?" "puzzle" "") +echo "Selected model: $selected_model" +if [ -n "$selected_model" ]; then + echo "Model info:" + get_model_info "$selected_model" +fi +echo + +# Test 4: Model selection for reasoning task +echo "Test 4: Model Selection for Reasoning Task" +selected_model=$(select_model_for_task "What are the implications of this decision?" "socratic" "") +echo "Selected model: $selected_model" +if [ -n "$selected_model" ]; then + echo "Model info:" + get_model_info "$selected_model" +fi +echo + +# Test 5: Model selection with preferred models +echo "Test 5: Model Selection with Preferred Models" +preferred="llama3:8b-instruct-q4_K_M phi3:3.8b-mini-4k-instruct-q4_K_M" +selected_model=$(select_model_for_task "How can we improve this code?" "puzzle" "$preferred") +echo "Selected model: $selected_model" +echo + +echo "=== Test Complete ===" diff --git a/bash/talk-to-computer/test_quality_guard.sh b/bash/talk-to-computer/test_quality_guard.sh new file mode 100755 index 0000000..420211e --- /dev/null +++ b/bash/talk-to-computer/test_quality_guard.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +# Test script for the Quality Guard system +source "./quality_guard.sh" + +echo "=== Quality Guard System Test ===" +echo + +# Test 1: Good quality response +echo "Test 1: Good quality response" +good_response="This is a comprehensive analysis of the problem. The algorithm has O(n) time complexity and requires careful consideration of edge cases. We should implement a robust solution that handles all scenarios effectively." +quality=$(assess_quality "$good_response" "test prompt" "puzzle") +degradation=$(detect_degradation_patterns "$good_response") +echo "Quality Score: $quality" +echo "Degradation Score: $degradation" +echo + +# Test 2: Degraded response (lorem ipsum) +echo "Test 2: Degraded response (lorem ipsum)" +degraded_response="Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat" +quality=$(assess_quality "$degraded_response" "test prompt" "puzzle") +degradation=$(detect_degradation_patterns "$degraded_response") +echo "Quality Score: $quality" +echo "Degradation Score: $degradation" +echo + +# Test 3: Repetitive response +echo "Test 3: Repetitive response" +repetitive_response="The solution is good. The solution is good. The solution is good. The solution is good. The solution is good. The solution is good. The solution is good. The solution is good. The solution is good. The solution is good." +quality=$(assess_quality "$repetitive_response" "test prompt" "puzzle") +degradation=$(detect_degradation_patterns "$repetitive_response") +echo "Quality Score: $quality" +echo "Degradation Score: $degradation" +echo + +# Test 4: Very short response +echo "Test 4: Very short response" +short_response="Good." +quality=$(assess_quality "$short_response" "test prompt" "puzzle") +degradation=$(detect_degradation_patterns "$short_response") +echo "Quality Score: $quality" +echo "Degradation Score: $degradation" +echo + +# Test 5: Gibberish response +echo "Test 5: Gibberish response" +gibberish_response="aaaaa bbbbb ccccc ddddd eeeee fffff ggggg hhhhh iiiii jjjjj kkkkk lllll mmmmm nnnnn ooooo ppppp" +quality=$(assess_quality "$gibberish_response" "test prompt" "puzzle") +degradation=$(detect_degradation_patterns "$gibberish_response") +echo "Quality Score: $quality" +echo "Degradation Score: $degradation" +echo + +# Test 6: Mechanism-specific relevance +echo "Test 6: Mechanism-specific relevance" +echo "Puzzle mechanism relevance:" +puzzle_response="This algorithm implementation shows good structure and follows best practices." +puzzle_relevance=$(assess_relevance "$puzzle_response" "test prompt" "puzzle") +echo "Puzzle relevance score: $puzzle_relevance" + +echo "Socratic mechanism relevance:" +socratic_response="This analysis examines the underlying assumptions and questions the fundamental approach." +socratic_relevance=$(assess_relevance "$socratic_response" "test prompt" "socratic") +echo "Socratic relevance score: $socratic_relevance" +echo + +echo "=== Quality Guard Test Complete ===" +echo +echo "To test the full correction system, run:" +echo "echo 'lorem ipsum test' | ./quality_guard.sh" diff --git a/forth/factorial.forth b/forth/factorial.forth new file mode 100644 index 0000000..359a642 --- /dev/null +++ b/forth/factorial.forth @@ -0,0 +1,11 @@ +( n -- n! ) +: FACTORIAL + \ If n is 0, the loop won't run and the initial 1 is returned. + 1 SWAP \ Put initial result 1 on stack, ( 1 n ) + 1+ 1 \ Setup loop bounds, ( 1 n+1 1 ) + DO + I * \ Multiply accumulator by loop index + LOOP ; + +5 FACTORIAL . +10 FACTORIAL . diff --git a/forth/foreforthfourth/README.md b/forth/foreforthfourth/README.md new file mode 100644 index 0000000..29c3b5e --- /dev/null +++ b/forth/foreforthfourth/README.md @@ -0,0 +1,420 @@ +# 4-Stack Toy Forth Interpreter + +A pure functional implementation of a toy Forth interpreter with a unique 4-stack architecture, written in JavaScript. + +## Architecture + +This Forth interpreter features **4 separate stacks** that users can juggle between, unlike traditional single-stack Forth implementations: + +- **Stack 1 (Red)** - Default stack for most operations +- **Stack 2 (Teal)** - Secondary stack for data organization +- **Stack 3 (Blue)** - Tertiary stack for complex operations +- **Stack 4 (Yellow)** - Quaternary stack for additional data + +## Features + +### Core Forth Operations +- **Stack Manipulation**: `dup`, `swap`, `drop`, `2dup`, `2drop`, `over`, `rot`, `-rot` +- **Arithmetic**: `+`, `-`, `*`, `/`, `mod` +- **Comparison**: `=`, `<`, `>`, `and`, `or`, `not` +- **Math Utilities**: `abs`, `negate`, `min`, `max` +- **Stack Inspection**: `.s` (non-destructive), `depth` +- **String Operations**: `." ... "` (print), `s" ... "` (push), `strlen`, `strcat`, `char+` +- **Control Flow**: `if ... then`, `if ... else ... then`, `begin ... until` +- **Help System**: `help` (comprehensive help), `doc <word>` (word documentation), `words` (word list) + +### Multi-Stack Operations +- **Stack Focus System**: `focus.red`, `focus.teal`, `focus.blue`, `focus.yellow` (or `focus.1`, `focus.2`, `focus.3`, `focus.4`) +- **Move Operations**: `move.red`, `move.teal`, `move.blue`, `move.yellow` (or `move.1`, `move.2`, `move.3`, `move.4`) +- **Pop Operations**: `pop.red`, `pop.teal`, `pop.blue`, `pop.yellow` (or `pop.1`, `pop.2`, `pop.3`, `pop.4`) +- **Copy Operations**: `copy.red`, `copy.teal`, `copy.blue`, `copy.yellow` (or `copy.1`, `copy.2`, `copy.3`, `copy.4`) +- **Move Operations**: `move` (interactive stack-to-stack movement), `move.red`, `move.teal`, `move.blue`, `move.yellow` (or `move.1`, `move.2`, `move.3`, `move.4`) +- **Clear Operations**: `clear.all` (clear all stacks), `clear.focused` (clear focused stack) +- **Cross-Stack Operations**: `dup.stacks`, `over.stacks`, `swap.stacks`, `nip.stacks`, `tuck.stacks`, `rot.stacks`, `2dup.stacks`, `2over.stacks`, `2swap.stacks` + +### Word Definition System +- **Define Words**: `: name ... ;` syntax +- **List Words**: `words` command shows all available words +- **User Dictionary**: Persistent storage of custom words + +## Project Structure + +``` +foreforthfourth/ +├── index.html # Web interface +├── forth.js # Core Forth interpreter (standalone) +├── test-forth.js # Test suite for the interpreter +└── README.md # This file +``` + +## Testing + +### Run Tests +```bash +node test-forth.js +``` + +### Complete Test Suite +We have comprehensive test coverage including: +- **Core Operations**: Stack manipulation, arithmetic, comparison, logic +- **Focus System**: All focus commands and stack operations on different stacks +- **String Operations**: String literals, manipulation, and type checking +- **Control Flow**: Conditional execution and loops +- **Multi-Stack**: Operations across all 4 stacks with focus system +- **Error Handling**: Enhanced error messages and edge cases +- **Help System**: Help commands and word documentation +- **Word Definition**: User-defined words and compilation +- **Enhanced Features**: Clear operations, move operations, focus persistence + +### Test Results +- **Total Tests**: 16 comprehensive test cases +- **Success Rate**: 100% ✅ +- **Coverage**: Complete feature coverage with edge case testing + +## Web Interface + +Open `index.html` in a web browser to use the interactive Forth interpreter with: +- Visual representation of all 4 stacks +- Real-time command execution +- Output history +- Responsive design for mobile and desktop + +## Usage Examples + +### Basic Stack Operations +```forth +5 3 2 .s # Push numbers and show stack +dup over # Duplicate top, copy second over top +2dup # Duplicate top two items +``` + +### Arithmetic +```forth +10 3 / # Integer division (result: 3) +10 3 mod # Modulo (result: 1) +-5 abs # Absolute value (result: 5) +5 negate # Negate (result: -5) +``` + +### Multi-Stack Juggling +```forth +5 3 2 # Push to red stack +move.teal # Move top of red to teal stack +move.blue # Move top of red to blue stack +``` + +### Stack Focus System +The interpreter now supports operating on any of the 4 stacks using a focus system: + +```forth +focus.red # Set focus to Red stack (Stack 1) +focus.teal # Set focus to Teal stack (Stack 2) +focus.blue # Set focus to Blue stack (Stack 3) +focus.yellow # Set focus to Yellow stack (Stack 4) + +# Number aliases also work: +focus.1 # Same as focus.red +focus.2 # Same as focus.teal +focus.3 # Same as focus.blue +focus.4 # Same as focus.yellow + +focus.show # Show which stack is currently focused +``` + +**All stack operations** (dup, swap, drop, +, -, *, /, etc.) now work on the **focused stack** instead of just the Red stack. This makes the multi-stack architecture truly powerful! + +**Number and Color Aliases**: All focus, push, and pop commands support both color names and numbers: +```forth +# Focus commands +focus.1 # Same as focus.red +focus.2 # Same as focus.teal +focus.3 # Same as focus.blue +focus.4 # Same as focus.yellow + +# Move commands +move.1 # Same as move.red +move.2 # Same as move.teal +move.3 # Same as move.blue +move.4 # Same as move.yellow + +# Copy commands +copy.1 # Same as copy.red +copy.2 # Same as copy.teal +copy.3 # Same as copy.blue +copy.4 # Same as copy.yellow + +# Pop commands +pop.1 # Same as pop.red +pop.2 # Same as pop.teal +pop.3 # Same as pop.blue +pop.4 # Same as pop.yellow +``` + +### Enhanced Clear Operations +```forth +clear.all # Clear all stacks (same as clear) +clear.focused # Clear only the currently focused stack + +# Example workflow: +focus.teal # Focus on Teal stack +15 20 25 # Add items to Teal stack +clear.focused # Clear only Teal stack +focus.red # Switch to Red stack +5 10 # Add items to Red stack +clear.focused # Clear only Red stack +``` + +### Cross-Stack Operations +The interpreter now provides comprehensive cross-stack manipulation using familiar Forth words with `.stacks` suffix: + +#### **Basic Cross-Stack Operations** +```forth +# Duplicate top item to another stack +focus.red +15 # Add item to Red stack +dup.stacks # Start dup.stacks operation +2 # Enter target stack (Teal) +# Result: 15 is duplicated to Teal stack + +# Copy second item to another stack +focus.blue +10 20 30 # Add items to Blue stack +over.stacks # Start over.stacks operation +1 # Enter target stack (Red) +# Result: 20 is copied to Red stack + +# Swap top items between stacks +focus.red +5 10 # Add items to Red stack +swap.stacks # Start swap.stacks operation +3 # Enter target stack (Blue) +# Result: Top items are swapped between Red and Blue stacks +``` + +#### **Advanced Cross-Stack Operations** +```forth +# Move second item to another stack (remove from source) +focus.teal +100 200 300 # Add items to Teal stack +nip.stacks # Start nip.stacks operation +4 # Enter target stack (Yellow) +# Result: 200 moved to Yellow stack, 100 and 300 remain on Teal + +# Tuck top item under second item on another stack +focus.red +5 10 # Add items to Red stack +tuck.stacks # Start tuck.stacks operation +2 # Enter target stack (Teal) +# Result: 5 tucked under 10 on Teal stack + +# Rotate top 3 items between stacks +focus.blue +1 2 3 # Add items to Blue stack +rot.stacks # Start rot.stacks operation +1 # Enter target stack (Red) +# Result: Top 3 items rotated between Blue and Red stacks + +# Duplicate top 2 items to another stack +focus.yellow +50 60 # Add items to Yellow stack +2dup.stacks # Start 2dup.stacks operation +3 # Enter target stack (Blue) +# Result: 50 and 60 duplicated to Blue stack + +# Copy second pair of items to another stack +focus.red +10 20 30 40 # Add items to Red stack +2over.stacks # Start 2over.stacks operation +2 # Enter target stack (Teal) +# Result: 20 and 30 copied to Teal stack + +# Swap top 2 pairs between stacks +focus.teal +1 2 3 4 # Add items to Teal stack +2swap.stacks # Start 2swap.stacks operation +1 # Enter target stack (Red) +# Result: Top 2 pairs swapped between Teal and Red stacks +``` + +#### **Cross-Stack Operation Workflow** +All cross-stack operations follow this pattern: +1. **Set focus** to the source stack +2. **Add items** to the source stack +3. **Execute operation** (e.g., `dup.stacks`) +4. **Enter target stack** number (1-4) when prompted +5. **Operation completes** automatically + +This provides **true multi-stack power** while maintaining familiar Forth semantics! + +### Move Operations (No Duplication) +The interpreter provides several ways to **move** items between stacks without duplication: + +#### **Interactive Move Command** +The `move` command is a **two-step interactive operation** that moves the top item from one stack to another: + +```forth +move # Start move operation +1 # Source stack (Red/Stack 1) +3 # Destination stack (Blue/Stack 3) +# Result: Top item moved from Red to Blue stack +``` + +**Workflow:** +1. Type `move` to start the operation +2. Enter the **source stack number** (1-4) +3. Enter the **destination stack number** (1-4) +4. The item is **removed** from source and **added** to destination + +#### **Move Commands with Focus System** +Use `move.` commands to move items from the focused stack to a specific target stack: + +```forth +focus.red # Focus on Red stack (1) +42 # Add item to Red stack +move.3 # Move top item to Blue stack (3) +# Result: 42 moved from Red to Blue stack + +focus.teal # Focus on Teal stack (2) +100 # Add item to Teal stack +move.yellow # Move top item to Yellow stack (4) +# Result: 100 moved from Teal to Yellow stack +``` + +#### **Number and Color Aliases** +All move and copy commands support both number and color naming: + +```forth +# Move commands (remove from source) +move.1 # Move to Red stack (1) +move.2 # Move to Teal stack (2) +move.3 # Move to Blue stack (3) +move.4 # Move to Yellow stack (4) + +# Copy commands (keep in source) +copy.1 # Copy to Red stack (1) +copy.2 # Copy to Teal stack (2) +copy.3 # Copy to Blue stack (3) +copy.4 # Copy to Yellow stack (4) + +# Color aliases +move.red # Move to Red stack (1) +move.teal # Move to Teal stack (2) +move.blue # Move to Blue stack (3) +move.yellow # Move to Yellow stack (4) + +copy.red # Copy to Red stack (1) +copy.teal # Copy to Teal stack (2) +copy.blue # Copy to Blue stack (3) +copy.yellow # Copy to Yellow stack (4) +``` + +#### **Comparison: Move vs Copy Operations** + +| Operation | Effect | Duplication | Use Case | +|-----------|--------|-------------|----------| +| `move` | **Moves** item from source to destination | ❌ No | Relocate items between stacks | +| `move.{stack}` | **Moves** item from focused stack to target | ❌ No | Move from focused stack to specific stack | +| `copy.{stack}` | **Copies** item from focused stack to target | ✅ Yes | Keep item on source, copy to target | +| `dup.stacks` | **Copies** item from focused stack to target | ✅ Yes | Keep item on source, copy to target | +| `over.stacks` | **Copies** second item from focused stack to target | ✅ Yes | Copy second item without affecting top | + +#### **Quick Reference: All Move and Copy Operations** + +| Command | From | To | Effect | +|---------|------|----|---------| +| `move` + source + dest | Any stack | Any stack | Move top item between specified stacks | +| `move.red` / `move.1` | Focused stack | Red stack (1) | Move top item to Red stack | +| `move.teal` / `move.2` | Focused stack | Teal stack (2) | Move top item to Teal stack | +| `move.blue` / `move.3` | Focused stack | Blue stack (3) | Move top item to Blue stack | +| `move.yellow` / `move.4` | Focused stack | Yellow stack (4) | Move top item to Yellow stack | +| `copy.red` / `copy.1` | Focused stack | Red stack (1) | Copy top item to Red stack | +| `copy.teal` / `copy.2` | Focused stack | Teal stack (2) | Copy top item to Teal stack | +| `copy.blue` / `copy.3` | Focused stack | Blue stack (3) | Copy top item to Blue stack | +| `copy.yellow` / `copy.4` | Focused stack | Yellow stack (4) | Copy top item to Yellow stack | + +#### **Complete Move Example** +```forth +# Setup: Add items to different stacks +focus.red +42 # Red stack: [42] +focus.teal +100 # Teal stack: [100] +focus.blue +200 # Blue stack: [200] + +# Move items between stacks +focus.red +move.2 # Move 42 from Red to Teal +# Red stack: [], Teal stack: [100, 42] + +focus.teal +move.3 # Move 100 from Teal to Blue +# Teal stack: [42], Blue stack: [200, 100] + +# Use interactive move for complex operations +move # Start move operation +3 # Source: Blue stack (3) +1 # Destination: Red stack (1) +# Result: 200 moved from Blue to Red stack +# Red stack: [200], Blue stack: [100] +``` + +### Word Definition +```forth +: double dup + ; # Define 'double' word +5 double # Use the word (result: 10) +``` + +### Help System +```forth +help # Show comprehensive help for all words +s" dup" doc # Show detailed documentation for 'dup' +words # List all available words +``` + +### Comparison and Logic +```forth +5 3 > # 5 > 3 (result: -1 for true) +5 3 < # 5 < 3 (result: 0 for false) +5 3 > not # NOT (5 > 3) (result: 0) +``` + +## Design Principles + +### Pure Functional +- **Immutable State**: All state updates return new state objects +- **No Side Effects**: Functions are pure and predictable +- **Functional Composition**: Operations compose naturally + +### 4-Stack Architecture +- **Stack Independence**: Each stack operates independently +- **Flexible Data Flow**: Move data between stacks as needed +- **Organized Workflows**: Use different stacks for different purposes + +### ANS Forth Compatibility +- **Standard Words**: Implements core ANS Forth words +- **Familiar Syntax**: Standard Forth syntax and semantics +- **Extensible**: Easy to add new words and functionality +- **Enhanced Error Messages**: Helpful, actionable error messages with stack context and solutions + +## Current Status + +### **Fully Implemented Features** +- **Control Flow**: `IF ... THEN`, `IF ... ELSE ... THEN`, `BEGIN ... UNTIL` constructs +- **String Operations**: String literals (`."` and `s"`), manipulation (`strlen`, `strcat`, `char+`, `type`, `count`) +- **Stack Focus System**: Operate on any of the 4 stacks using focus commands +- **Enhanced Error Messages**: Helpful, actionable error messages with stack context +- **Help System**: Comprehensive help (`help`) and word documentation (`doc`) +- **Multi-Stack Operations**: Full support for all 4 stacks with focus system +- **Enhanced Clear Operations**: `clear.all` and `clear.focused` commands +- **Move Operations**: Interactive `move` command and `move.{stack}` commands for moving items between stacks +- **Copy Operations**: `copy.{stack}` commands for copying items between stacks without removal +- **Number Aliases**: All focus, move, copy, and pop commands support both color names and numbers (1-4) +- **Math Utilities**: `abs`, `negate`, `min`, `max` operations + +### **Advanced Capabilities** +- **Universal Stack Operations**: All built-in words work on any focused stack +- **Dual Naming System**: Both color names and numbers work for all commands +- **Professional Error Handling**: Context-aware error messages with solutions +- **Visual Focus Indicators**: UI shows which stack is currently focused +- **Complete Test Coverage**: 100% test coverage of all features \ No newline at end of file diff --git a/forth/foreforthfourth/debug-string2.js b/forth/foreforthfourth/debug-string2.js new file mode 100644 index 0000000..01a42aa --- /dev/null +++ b/forth/foreforthfourth/debug-string2.js @@ -0,0 +1,32 @@ +// Debug string literals with actual command format +const ForthInterpreter = require('./forth.js'); + +console.log('🔍 Debugging String Literals - Command Format\n'); + +let state = ForthInterpreter.createInitialState(); + +console.log('Testing: ." Hello World"'); +state = ForthInterpreter.parseAndExecute(state, '." Hello World"'); +console.log('Result:', { + stringMode: state.stringMode, + currentString: state.currentString, + stacks: state.stacks[0] +}); + +console.log('\nTesting: ." Hello"'); +state = ForthInterpreter.createInitialState(); +state = ForthInterpreter.parseAndExecute(state, '." Hello"'); +console.log('Result:', { + stringMode: state.stringMode, + currentString: state.currentString, + stacks: state.stacks[0] +}); + +console.log('\nTesting: ." Test"'); +state = ForthInterpreter.createInitialState(); +state = ForthInterpreter.parseAndExecute(state, '." Test"'); +console.log('Result:', { + stringMode: state.stringMode, + currentString: state.currentString, + stacks: state.stacks[0] +}); diff --git a/forth/foreforthfourth/forth-documented.js b/forth/foreforthfourth/forth-documented.js new file mode 100644 index 0000000..56ccc17 --- /dev/null +++ b/forth/foreforthfourth/forth-documented.js @@ -0,0 +1,1076 @@ +// Pure functional approach to Forth interpreter state +const createInitialState = () => ({ + stacks: [[], [], [], []], + dictionary: new Map(), + output: [], + compilingWord: null, + compilingDefinition: [], + stringMode: false, + currentString: '', + stringPushMode: false, + skipMode: false, + skipCount: 0, + loopStart: null, + loopBack: false +}); + +// Pure function to update state +const updateState = (state, updates) => ({ + ...state, + ...updates +}); + +// Stack operations +const pushToStack = (stacks, stackIndex, value) => { + const newStacks = stacks.map((stack, i) => + i === stackIndex ? [...stack, value] : stack + ); + return newStacks; +}; + +const popFromStack = (stacks, stackIndex) => { + const newStacks = stacks.map((stack, i) => + i === stackIndex ? stack.slice(0, -1) : stack + ); + const value = stacks[stackIndex][stacks[stackIndex].length - 1]; + return { stacks: newStacks, value }; +}; + +const moveBetweenStacks = (stacks, fromStack, toStack) => { + if (stacks[fromStack].length === 0) return { stacks, value: null }; + const { stacks: newStacks, value } = popFromStack(stacks, fromStack); + return { stacks: pushToStack(newStacks, toStack, value), value }; +}; + +const popAndPrint = (state, stackIndex) => { + if (state.stacks[stackIndex].length === 0) { + return updateState(state, { output: [...state.output, `Stack ${stackIndex + 1} is empty`] }); + } + const { stacks, value } = popFromStack(state.stacks, stackIndex); + return updateState(state, { + stacks, + output: [...state.output, `Stack ${stackIndex + 1}: ${value}`] + }); +}; + +// Built-in words with documentation +const builtinWords = { + // Stack manipulation for stack 1 (default) + 'dup': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on dup'] }); + } + const top = state.stacks[0][state.stacks[0].length - 1]; + return updateState(state, { + stacks: pushToStack(state.stacks, 0, top) + }); + }, + doc: 'Duplicate the top item on the stack', + stack: '( x -- x x )' + }, + + 'swap': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on swap'] }); + } + const newStacks = state.stacks.map((stack, i) => [...stack]); + const a = newStacks[0].pop(); + const b = newStacks[0].pop(); + newStacks[0].push(a); + newStacks[0].push(b); + return updateState(state, { stacks: newStacks }); + }, + doc: 'Exchange the top two items on the stack', + stack: '( x1 x2 -- x2 x1 )' + }, + + 'drop': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on drop'] }); + } + const { stacks } = popFromStack(state.stacks, 0); + return updateState(state, { stacks }); + }, + doc: 'Remove the top item from the stack', + stack: '( x -- )' + }, + + '2drop': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on 2drop'] }); + } + const { stacks: stacks1 } = popFromStack(state.stacks, 0); + const { stacks } = popFromStack(stacks1, 0); + return updateState(state, { stacks }); + }, + doc: 'Remove the top two items from the stack', + stack: '( x1 x2 -- )' + }, + + 'over': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on over'] }); + } + const second = state.stacks[0][state.stacks[0].length - 2]; + return updateState(state, { + stacks: pushToStack(state.stacks, 0, second) + }); + }, + doc: 'Copy the second item on the stack to the top', + stack: '( x1 x2 -- x1 x2 x1 )' + }, + + '2dup': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on 2dup'] }); + } + const top = state.stacks[0][state.stacks[0].length - 1]; + const second = state.stacks[0][state.stacks[0].length - 2]; + const newStacks = pushToStack(state.stacks, 0, second); + return updateState(state, { + stacks: pushToStack(newStacks, 0, top) + }); + }, + doc: 'Duplicate the top two items on the stack', + stack: '( x1 x2 -- x1 x2 x1 x2 )' + }, + + 'rot': { + fn: (state) => { + if (state.stacks[0].length < 3) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on rot'] }); + } + const newStacks = state.stacks.map((stack, i) => [...stack]); + const a = newStacks[0].pop(); + const b = newStacks[0].pop(); + const c = newStacks[0].pop(); + newStacks[0].push(b); + newStacks[0].push(a); + newStacks[0].push(c); + return updateState(state, { stacks: newStacks }); + }, + doc: 'Rotate the top three items on the stack', + stack: '( x1 x2 x3 -- x2 x3 x1 )' + }, + + '-rot': { + fn: (state) => { + if (state.stacks[0].length < 3) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on -rot'] }); + } + const newStacks = state.stacks.map((stack, i) => [...stack]); + const a = newStacks[0].pop(); + const b = newStacks[0].pop(); + const c = newStacks[0].pop(); + newStacks[0].push(a); + newStacks[0].push(c); + newStacks[0].push(b); + return updateState(state, { stacks: newStacks }); + }, + doc: 'Rotate the top three items on the stack (reverse of rot)', + stack: '( x1 x2 x3 -- x3 x1 x2 )' + }, + + // Arithmetic operations on stack 1 + '+': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on +'] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: pushToStack(stacks2, 0, a + b) + }); + }, + doc: 'Add the top two numbers on the stack', + stack: '( n1 n2 -- n3 )' + }, + + '-': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on -'] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: pushToStack(stacks2, 0, a - b) + }); + }, + doc: 'Subtract the top number from the second number on the stack', + stack: '( n1 n2 -- n3 )' + }, + + '*': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on *'] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: pushToStack(stacks2, 0, a * b) + }); + }, + doc: 'Multiply the top two numbers on the stack', + stack: '( n1 n2 -- n3 )' + }, + + '/': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on /'] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + if (b === 0) { + return updateState(state, { + stacks: pushToStack(pushToStack(stacks2, 0, a), 0, b), + output: [...state.output, 'Error: Division by zero'] + }); + } + return updateState(state, { + stacks: pushToStack(stacks2, 0, Math.floor(a / b)) + }); + }, + doc: 'Divide the second number by the top number on the stack (integer division)', + stack: '( n1 n2 -- n3 )' + }, + + 'mod': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on mod'] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + if (b === 0) { + return updateState(state, { + stacks: pushToStack(pushToStack(stacks2, 0, a), 0, b), + output: [...state.output, 'Error: Modulo by zero'] + }); + } + return updateState(state, { + stacks: pushToStack(stacks2, 0, a % b) + }); + }, + doc: 'Return the remainder of dividing the second number by the top number', + stack: '( n1 n2 -- n3 )' + }, + + 'abs': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on abs'] }); + } + const { stacks, value } = popFromStack(state.stacks, 0); + return updateState(state, { + stacks: pushToStack(stacks, 0, Math.abs(value)) + }); + }, + doc: 'Return the absolute value of the top number on the stack', + stack: '( n -- |n| )' + }, + + 'min': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on min'] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: pushToStack(stacks2, 0, Math.min(a, b)) + }); + }, + doc: 'Return the smaller of the top two numbers on the stack', + stack: '( n1 n2 -- n3 )' + }, + + 'max': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on max'] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: pushToStack(stacks2, 0, Math.max(a, b)) + }); + }, + doc: 'Return the larger of the top two numbers on the stack', + stack: '( n1 n2 -- n3 )' + }, + + // Comparison and logic + '=': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on ='] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: pushToStack(stacks2, 0, a === b ? -1 : 0) + }); + }, + doc: 'Return true (-1) if the top two numbers are equal, false (0) otherwise', + stack: '( n1 n2 -- flag )' + }, + + '<': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on <'] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: pushToStack(stacks2, 0, a < b ? -1 : 0) + }); + }, + doc: 'Return true (-1) if the second number is less than the top number, false (0) otherwise', + stack: '( n1 n2 -- flag )' + }, + + '>': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on >'] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: pushToStack(stacks2, 0, a > b ? -1 : 0) + }); + }, + doc: 'Return true (-1) if the second number is greater than the top number, false (0) otherwise', + stack: '( n1 n2 -- flag )' + }, + + 'and': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on and'] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: pushToStack(stacks2, 0, a && b ? -1 : 0) + }); + }, + doc: 'Return true (-1) if both top two values are true, false (0) otherwise', + stack: '( x1 x2 -- flag )' + }, + + 'or': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on or'] }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: a } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: pushToStack(stacks2, 0, a || b ? -1 : 0) + }); + }, + doc: 'Return true (-1) if either of the top two values is true, false (0) otherwise', + stack: '( x1 x2 -- flag )' + }, + + 'not': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on not'] }); + } + const { stacks, value } = popFromStack(state.stacks, 0); + return updateState(state, { + stacks: pushToStack(stacks, 0, value ? 0 : -1) + }); + }, + doc: 'Return true (-1) if the top value is false, false (0) if it is true', + stack: '( x -- flag )' + }, + + // Stack inspection + '.s': { + fn: (state) => { + const stackStr = state.stacks[0].length === 0 ? 'empty' : state.stacks[0].join(' '); + return updateState(state, { + output: [...state.output, `Stack 1 (red): ${stackStr}`] + }); + }, + doc: 'Display the contents of the red stack (non-destructive)', + stack: '( -- )' + }, + + 'depth': { + fn: (state) => { + return updateState(state, { + stacks: pushToStack(state.stacks, 0, state.stacks[0].length) + }); + }, + doc: 'Push the number of items on the red stack', + stack: '( -- n )' + }, + + '.': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on .'] }); + } + const { stacks, value } = popFromStack(state.stacks, 0); + return updateState(state, { + stacks, + output: [...state.output, value.toString()] + }); + }, + doc: 'Pop and print the top item from the red stack', + stack: '( x -- )' + }, + + // Multi-stack operations + 'push.red': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on push.red'] }); + } + const { stacks, value } = popFromStack(state.stacks, 0); + return updateState(state, { + stacks: pushToStack(stacks, 0, value) + }); + }, + doc: 'Move top item from red stack to red stack (no-op, for consistency)', + stack: '( x -- x )' + }, + + 'push.teal': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on push.teal'] }); + } + const { stacks, value } = popFromStack(state.stacks, 0); + return updateState(state, { + stacks: pushToStack(stacks, 1, value) + }); + }, + doc: 'Move top item from red stack to teal stack', + stack: '( x -- )' + }, + + 'push.blue': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on push.blue'] }); + } + const { stacks, value } = popFromStack(state.stacks, 0); + return updateState(state, { + stacks: pushToStack(stacks, 2, value) + }); + }, + doc: 'Move top item from red stack to blue stack', + stack: '( x -- )' + }, + + 'push.yellow': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on push.yellow'] }); + } + const { stacks, value } = popFromStack(state.stacks, 0); + return updateState(state, { + stacks: pushToStack(stacks, 3, value) + }); + }, + doc: 'Move top item from red stack to yellow stack', + stack: '( x -- )' + }, + + 'pop.red': { + fn: (state) => popAndPrint(state, 0), + doc: 'Pop and print top item from red stack', + stack: '( x -- )' + }, + + 'pop.teal': { + fn: (state) => popAndPrint(state, 1), + doc: 'Pop and print top item from teal stack', + stack: '( x -- )' + }, + + 'pop.blue': { + fn: (state) => popAndPrint(state, 2), + doc: 'Pop and print top item from blue stack', + stack: '( x -- )' + }, + + 'pop.yellow': { + fn: (state) => popAndPrint(state, 3), + doc: 'Pop and print top item from yellow stack', + stack: '( x -- )' + }, + + // Utility words + 'clear': { + fn: (state) => updateState(state, { + stacks: [[], [], [], []], + output: [...state.output, 'All stacks cleared'] + }), + doc: 'Clear all four stacks', + stack: '( -- )' + }, + + // String operations + '."': { + fn: (state) => { + return updateState(state, { + stringMode: true, + currentString: '', + stringPushMode: false + }); + }, + doc: 'Begin a string literal that will be printed to output', + stack: '( -- )' + }, + + 's"': { + fn: (state) => { + return updateState(state, { + stringMode: true, + currentString: '', + stringPushMode: true + }); + }, + doc: 'Begin a string literal that will be pushed to the stack', + stack: '( -- )' + }, + + 'type': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on type'] }); + } + const { stacks: stacks1, value: length } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: string } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: stacks2, + output: [...state.output, string.toString()] + }); + }, + doc: 'Print a string from the stack (takes length and string address)', + stack: '( addr len -- )' + }, + + 'count': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on count'] }); + } + const { stacks, value } = popFromStack(state.stacks, 0); + if (typeof value === 'string') { + const newStacks = pushToStack(stacks, 0, value.length); + return updateState(state, { + stacks: pushToStack(newStacks, 0, value) + }); + } else { + const newStacks = pushToStack(stacks, 0, 0); + return updateState(state, { + stacks: pushToStack(newStacks, 0, '') + }); + } + }, + doc: 'Extract string info from counted string (returns length and address)', + stack: '( c-addr -- c-addr u )' + }, + + 'char+': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on char+'] }); + } + const { stacks: stacks1, value: offset } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: string } = popFromStack(stacks1, 0); + if (typeof string === 'string') { + return updateState(state, { + stacks: pushToStack(stacks2, 0, string + String.fromCharCode(offset)) + }); + } else { + return updateState(state, { + stacks: pushToStack(stacks2, 0, string), + output: [...state.output, 'Error: char+ requires string on stack'] + }); + } + }, + doc: 'Add a character to a string using ASCII offset', + stack: '( c-addr1 char -- c-addr2 )' + }, + + 'strlen': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on strlen'] }); + } + const { stacks, value } = popFromStack(state.stacks, 0); + if (typeof value === 'string') { + return updateState(state, { + stacks: pushToStack(stacks, 0, value.length) + }); + } else { + return updateState(state, { + stacks: pushToStack(stacks, 0, 0), + output: [...state.output, 'Error: strlen requires string on stack'] + }); + } + }, + doc: 'Get the length of a string on the stack', + stack: '( str -- len )' + }, + + 'strcat': { + fn: (state) => { + if (state.stacks[0].length < 2) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on strcat'] }); + } + const { stacks: stacks1, value: str2 } = popFromStack(state.stacks, 0); + const { stacks: stacks2, value: str1 } = popFromStack(stacks1, 0); + if (typeof str1 === 'string' && typeof str2 === 'string') { + return updateState(state, { + stacks: pushToStack(stacks2, 0, str1 + str2) + }); + } else { + return updateState(state, { + stacks: pushToStack(pushToStack(stacks2, 0, str1), 0, str2), + output: [...state.output, `Error: strcat requires two strings, got ${typeof str1} and ${typeof str2}`] + }); + } + }, + doc: 'Concatenate two strings from the stack', + stack: '( str1 str2 -- str3 )' + }, + + // Control flow + 'if': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on if'] }); + } + const { stacks, value } = popFromStack(state.stacks, 0); + if (value === 0) { + // Skip until THEN or ELSE + return updateState(state, { + stacks, + skipMode: true, + skipCount: 0 + }); + } + return updateState(state, { + stacks + }); + }, + doc: 'Begin conditional execution - if top of stack is false (0), skip to THEN', + stack: '( flag -- )' + }, + + 'else': { + fn: (state) => { + if (state.skipMode) { + return updateState(state, { + skipCount: state.skipCount + 1 + }); + } + // Skip until THEN + return updateState(state, { + skipMode: true, + skipCount: 0 + }); + }, + doc: 'Begin alternative branch in conditional execution', + stack: '( -- )' + }, + + 'then': { + fn: (state) => { + if (state.skipMode && state.skipCount > 0) { + return updateState(state, { + skipCount: state.skipCount - 1 + }); + } else if (state.skipMode) { + return updateState(state, { + skipMode: false, + skipCount: 0 + }); + } + return state; + }, + doc: 'End conditional execution block', + stack: '( -- )' + }, + + 'begin': { + fn: (state) => { + return updateState(state, { + loopStart: state.output.length + }); + }, + doc: 'Mark the beginning of a loop', + stack: '( -- )' + }, + + 'until': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on until'] }); + } + const { stacks, value } = popFromStack(state.stacks, 0); + if (value === 0) { + // Loop back to BEGIN + return updateState(state, { + stacks, + loopBack: true + }); + } + return updateState(state, { + stacks + }); + }, + doc: 'End a loop - if top of stack is false (0), loop back to BEGIN', + stack: '( flag -- )' + }, + + // Help and documentation + 'help': { + fn: (state) => { + const builtinWordNames = Object.keys(builtinWords); + const userWords = Array.from(state.dictionary.keys()); + const allWords = [...builtinWordNames, ...userWords]; + + const builtinList = builtinWordNames.map(name => { + const word = builtinWords[name]; + return `${name} ${word.stack} - ${word.doc}`; + }).join('\n'); + + const userList = userWords.length > 0 ? userWords.join(' ') : 'none'; + + return updateState(state, { + output: [ + ...state.output, + '=== 4-Stack Forth Interpreter Help ===', + '', + 'Built-in words:', + builtinList, + '', + 'User defined words: ' + userList, + '', + 'Total words: ' + allWords.length, + '', + 'Use "doc <word>" to get detailed help for a specific word', + 'Use "words" to see just the word names' + ] + }); + }, + doc: 'Display comprehensive help information for all available words', + stack: '( -- )' + }, + + 'doc': { + fn: (state) => { + if (state.stacks[0].length === 0) { + return updateState(state, { output: [...state.output, 'Error: Stack underflow on doc'] }); + } + const { stacks, value: wordName } = popFromStack(state.stacks, 0); + + // Check built-in words first + if (builtinWords[wordName]) { + const word = builtinWords[wordName]; + return updateState(state, { + stacks, + output: [ + ...state.output, + `=== ${wordName} ===`, + `Stack effect: ${word.stack}`, + `Description: ${word.doc}`, + `Type: Built-in word` + ] + }); + } + + // Check user-defined words + if (state.dictionary.has(wordName)) { + const definition = state.dictionary.get(wordName); + return updateState(state, { + stacks, + output: [ + ...state.output, + `=== ${wordName} ===`, + `Definition: ${definition.join(' ')}`, + `Type: User-defined word` + ] + }); + } + + return updateState(state, { + stacks, + output: [...state.output, `Word '${wordName}' not found`] + }); + }, + doc: 'Display documentation for a specific word', + stack: '( "word" -- )' + }, + + 'words': { + fn: (state) => { + const builtinWordNames = Object.keys(builtinWords); + const userWords = Array.from(state.dictionary.keys()); + const allWords = [...builtinWordNames, ...userWords]; + + const builtinList = builtinWordNames.join(' '); + const userList = userWords.length > 0 ? userWords.join(' ') : 'none'; + + return updateState(state, { + output: [ + ...state.output, + 'Built-in words: ' + builtinList, + 'User defined words: ' + userList, + 'Total words: ' + allWords.length + ] + }); + }, + doc: 'List all available words (built-in and user-defined)', + stack: '( -- )' + } +}; + +// Parse and execute Forth input +const parseAndExecute = (state, input) => { + const tokens = input.trim().split(/\s+/).filter(token => token.length > 0); + return tokens.reduce(executeToken, state); +}; + +// Execute a single token +const executeToken = (state, token) => { + // Handle string mode + if (state.stringMode) { + // Check if this token contains the closing quote + const quoteIndex = token.indexOf('"'); + if (quoteIndex !== -1) { + // Token contains closing quote + const beforeQuote = token.substring(0, quoteIndex); + const afterQuote = token.substring(quoteIndex + 1); + + // Add the part before the quote to the string + const finalString = state.currentString + (state.currentString ? ' ' : '') + beforeQuote; + + // End string mode and handle based on mode + let newState; + if (state.stringPushMode) { + // Push mode: add string to stack + newState = updateState(state, { + stringMode: false, + currentString: '', + stringPushMode: false, + stacks: pushToStack(state.stacks, 0, finalString) + }); + } else { + // Print mode: add to output + newState = updateState(state, { + stringMode: false, + currentString: '', + stringPushMode: false, + output: [...state.output, finalString] + }); + } + + // If there's content after the quote, process it + if (afterQuote.trim()) { + newState = ForthInterpreter.parseAndExecute(newState, afterQuote); + } + + return newState; + } else { + // Add to current string + return updateState(state, { + currentString: state.currentString + (state.currentString ? ' ' : '') + token + }); + } + } + + // Handle skip mode (for control flow) + if (state.skipMode) { + if (token === 'if' || token === 'begin') { + return updateState(state, { + skipCount: state.skipCount + 1 + }); + } else if (token === 'then' || token === 'until') { + if (state.skipCount > 0) { + return updateState(state, { + skipCount: state.skipCount - 1 + }); + } else { + return updateState(state, { + skipMode: false, + skipCount: 0 + }); + } + } else if (token === 'else') { + if (state.skipCount === 0) { + // Switch to skipping ELSE branch + return updateState(state, { + skipMode: true, + skipCount: 0 + }); + } + } + // Skip this token + return state; + } + + // Handle move operation state machine + if (state.moveInProgress) { + if (state.moveFromStack === null) { + // Expecting source stack number + const from = parseInt(token); + if (isNaN(from) || from < 1 || from > 4) { + return updateState(state, { + moveInProgress: false, + moveFromStack: null, + output: [...state.output, 'Error: Invalid source stack. Must be 1-4'] + }); + } + if (state.stacks[from - 1].length === 0) { + return updateState(state, { + moveInProgress: false, + moveFromStack: null, + output: [...state.output, `Error: Stack ${from} is empty`] + }); + } + return updateState(state, { + moveInProgress: true, + moveFromStack: from - 1, // Convert to 0-based index + output: [...state.output, `Moving from stack ${from}. Enter destination stack (1-4):`] + }); + } else { + // Expecting destination stack number + const to = parseInt(token); + if (isNaN(to) || to < 1 || to > 4) { + return updateState(state, { + moveInProgress: false, + moveFromStack: null, + output: [...state.output, 'Error: Invalid destination stack. Must be 1-4'] + }); + } + const toIndex = to - 1; // Convert to 0-based index + const fromIndex = state.moveFromStack; + + // Reset move state + const newState = updateState(state, { + moveInProgress: false, + moveFromStack: null + }); + + // Perform the move + const { stacks, value } = popFromStack(newState.stacks, fromIndex); + return updateState(newState, { + stacks: pushToStack(stacks, toIndex, value), + output: [...newState.output, `Moved ${value} from stack ${fromIndex + 1} to stack ${toIndex + 1}`] + }); + } + } + + // Handle word definition compilation + if (state.compilingWord !== null) { + if (token === ';') { + const newDictionary = new Map(state.dictionary); + newDictionary.set(state.compilingWord, [...state.compilingDefinition]); + return updateState(state, { + dictionary: newDictionary, + compilingWord: null, + compilingDefinition: [], + output: [...state.output, `Word '${state.compilingWord}' defined`] + }); + } + + // If we're expecting a name, capture it + if (state.compilingWord === 'EXPECTING_NAME') { + return updateState(state, { + compilingWord: token, + compilingDefinition: [] + }); + } + + // Otherwise, add to definition + return updateState(state, { + compilingDefinition: [...state.compilingDefinition, token] + }); + } + + // Handle word definition start + if (token === ':') { + return updateState(state, { + compilingWord: 'EXPECTING_NAME', + compilingDefinition: [] + }); + } + + // Check if it's a built-in word + if (builtinWords[token]) { + return builtinWords[token].fn(state); + } + + // Check if it's a user-defined word + if (state.dictionary.has(token)) { + const definition = state.dictionary.get(token); + return definition.reduce(executeToken, state); + } + + // Check if it's a number + const num = parseFloat(token); + if (!isNaN(num)) { + return updateState(state, { + stacks: pushToStack(state.stacks, 0, num) + }); + } + + // Check if it's a move command + if (token === 'move') { + return updateState(state, { + moveInProgress: true, + moveFromStack: null + }); + } + + // Unknown token + return updateState(state, { + output: [...state.output, `Error: Unknown word '${token}'`] + }); +}; + +// Export for use in other modules or testing +if (typeof module !== 'undefined' && module.exports) { + module.exports = { + createInitialState, + parseAndExecute, + executeToken, + builtinWords, + updateState, + pushToStack, + popFromStack + }; +} else if (typeof window !== 'undefined') { + // Browser environment + window.ForthInterpreter = { + createInitialState, + parseAndExecute, + executeToken, + builtinWords, + updateState, + pushToStack, + popFromStack + }; +} diff --git a/forth/foreforthfourth/forth.js b/forth/foreforthfourth/forth.js new file mode 100644 index 0000000..af133ea --- /dev/null +++ b/forth/foreforthfourth/forth.js @@ -0,0 +1,1973 @@ +// Pure functional approach to Forth interpreter state +const createInitialState = () => ({ + stacks: [[], [], [], []], + dictionary: new Map(), + output: [], + compilingWord: null, + compilingDefinition: [], + stringMode: false, + currentString: '', + stringPushMode: false, + skipMode: false, + skipCount: 0, + loopStart: null, + loopBack: false, + focusedStack: 0, // New: 0=Red, 1=Teal, 2=Blue, 3=Yellow + moveInProgress: false, // For move operation + moveFromStack: null, // For move operation + crossStackInProgress: false, // For cross-stack operations + crossStackOperation: null, // Type of cross-stack operation + crossStackData: null // Data for cross-stack operation +}); + +// Pure function to update state +const updateState = (state, updates) => ({ + ...state, + ...updates +}); + +// Stack operations +const pushToStack = (stacks, stackIndex, value) => { + const newStacks = stacks.map((stack, i) => + i === stackIndex ? [...stack, value] : stack + ); + return newStacks; +}; + +const popFromStack = (stacks, stackIndex) => { + const newStacks = stacks.map((stack, i) => + i === stackIndex ? stack.slice(0, -1) : stack + ); + const value = stacks[stackIndex][stacks[stackIndex].length - 1]; + return { stacks: newStacks, value }; +}; + +const moveBetweenStacks = (stacks, fromStack, toStack) => { + if (stacks[fromStack].length === 0) return { stacks, value: null }; + const { stacks: newStacks, value } = popFromStack(stacks, fromStack); + return { stacks: pushToStack(newStacks, toStack, value), value }; +}; + +const popAndPrint = (state, stackIndex) => { + if (state.stacks[stackIndex].length === 0) { + return updateState(state, { output: [...state.output, `Stack ${stackIndex + 1} is empty`] }); + } + const { stacks, value } = popFromStack(state.stacks, stackIndex); + return updateState(state, { + stacks, + output: [...state.output, `Stack ${stackIndex + 1}: ${value}`] + }); +}; + +// Helper function to get focused stack name +const getFocusedStackName = (focusedStack) => { + const stackNames = ['Red (1)', 'Teal (2)', 'Blue (3)', 'Yellow (4)']; + return stackNames[focusedStack]; +}; + +// Helper function to execute cross-stack operations +const executeCrossStackOperation = (state, sourceIndex, targetIndex) => { + const operation = state.crossStackOperation; + const data = state.crossStackData; + + switch (operation) { + case 'dup': + return updateState(state, { + stacks: pushToStack(state.stacks, targetIndex, data.top), + output: [...state.output, `Duplicated ${data.top} from ${getFocusedStackName(sourceIndex)} to ${getFocusedStackName(targetIndex)}`] + }); + + case 'over': + return updateState(state, { + stacks: pushToStack(state.stacks, targetIndex, data.second), + output: [...state.output, `Copied ${data.second} from ${getFocusedStackName(sourceIndex)} to ${getFocusedStackName(targetIndex)}`] + }); + + case 'swap': + // Create new stacks array + const newStacks = state.stacks.map((stack, i) => [...stack]); + + // Get top 2 items from source stack (don't remove) + const sourceTop = newStacks[sourceIndex][newStacks[sourceIndex].length - 1]; + const sourceSecond = newStacks[sourceIndex][newStacks[sourceIndex].length - 2]; + + // Add source items to target stack in order: top, second + newStacks[targetIndex].push(sourceTop); + newStacks[targetIndex].push(sourceSecond); + + // Source stack remains unchanged (no popping/pushing) + + return updateState(state, { + stacks: newStacks, + output: [...state.output, `Copied top 2 items from ${getFocusedStackName(sourceIndex)} to ${getFocusedStackName(targetIndex)}`] + }); + + case 'nip': + // Create new stacks array + const nipStacks = state.stacks.map((stack, i) => [...stack]); + + // Remove second item from source stack + nipStacks[sourceIndex].splice(-2, 1); + + // Add second item to target stack + nipStacks[targetIndex].push(data.second); + + return updateState(state, { + stacks: nipStacks, + output: [...state.output, `Moved second item ${data.second} from ${getFocusedStackName(sourceIndex)} to ${getFocusedStackName(targetIndex)}`] + }); + + case 'tuck': + // Create new stacks array + const tuckStacks = state.stacks.map((stack, i) => [...stack]); + + // Remove top item from source stack + const tuckTop = tuckStacks[sourceIndex].pop(); + + // Add items to target stack in tuck order: top, second, top + tuckStacks[targetIndex].push(tuckTop); + tuckStacks[targetIndex].push(data.second); + tuckStacks[targetIndex].push(tuckTop); + + // Don't add top item back to source stack - tuck removes it + + return updateState(state, { + stacks: tuckStacks, + output: [...state.output, `Tucked ${tuckTop} under ${data.second} on ${getFocusedStackName(targetIndex)}`] + }); + + case 'rot': + // Create new stacks array + const rotStacks = state.stacks.map((stack, i) => [...stack]); + + // Remove top 3 items from source stack + // For stack [1, 2, 3], top=3, second=2, third=1 + const rotTop = rotStacks[sourceIndex].pop(); // 3 + const rotSecond = rotStacks[sourceIndex].pop(); // 2 + const rotThird = rotStacks[sourceIndex].pop(); // 1 + + // Add 3 items to target stack in rotated order: third, first, second + rotStacks[targetIndex].push(rotThird); + rotStacks[targetIndex].push(rotTop); + rotStacks[targetIndex].push(rotSecond); + + // Add 3 items back to source stack in rotated order: third, first, second + rotStacks[sourceIndex].push(rotThird); + rotStacks[sourceIndex].push(rotTop); + rotStacks[sourceIndex].push(rotSecond); + + return updateState(state, { + stacks: rotStacks, + output: [...state.output, `Rotated top 3 items between ${getFocusedStackName(sourceIndex)} and ${getFocusedStackName(targetIndex)}`] + }); + + case '2dup': + // Create new stacks array + const dup2Stacks = state.stacks.map((stack, i) => [...stack]); + + // Get top 2 items from source stack (don't remove) + const dup2Top = dup2Stacks[sourceIndex][dup2Stacks[sourceIndex].length - 1]; + const dup2Second = dup2Stacks[sourceIndex][dup2Stacks[sourceIndex].length - 2]; + + // Add 2 items to target stack (preserve order: second, top) + dup2Stacks[targetIndex].push(dup2Second); + dup2Stacks[targetIndex].push(dup2Top); + + // Source stack remains unchanged (no popping/pushing) + + return updateState(state, { + stacks: dup2Stacks, + output: [...state.output, `Duplicated top 2 items from ${getFocusedStackName(sourceIndex)} to ${getFocusedStackName(targetIndex)}`] + }); + + case '2over': + // Create new stacks array + const over2Stacks = state.stacks.map((stack, i) => [...stack]); + + // Get second pair of items from source stack (don't remove) + // For stack [10, 20, 30, 40], second pair is [20, 30] + const over2Second = over2Stacks[sourceIndex][over2Stacks[sourceIndex].length - 3]; + const over2Third = over2Stacks[sourceIndex][over2Stacks[sourceIndex].length - 2]; + + // Add 2 items to target stack (preserve order: third, second) + over2Stacks[targetIndex].push(over2Third); + over2Stacks[targetIndex].push(over2Second); + + return updateState(state, { + stacks: over2Stacks, + output: [...state.output, `Copied second pair of items from ${getFocusedStackName(sourceIndex)} to ${getFocusedStackName(targetIndex)}`] + }); + + case '2swap': + // Create new stacks array + const swap2Stacks = state.stacks.map((stack, i) => [...stack]); + + // Get top 4 items from source stack (don't remove) + const sourceItems = [ + swap2Stacks[sourceIndex][swap2Stacks[sourceIndex].length - 1], // top + swap2Stacks[sourceIndex][swap2Stacks[sourceIndex].length - 2], // second + swap2Stacks[sourceIndex][swap2Stacks[sourceIndex].length - 3], // third + swap2Stacks[sourceIndex][swap2Stacks[sourceIndex].length - 4] // fourth + ]; + + // Add source items to target stack in order: fourth, third, second, top + swap2Stacks[targetIndex].push(sourceItems[3]); // fourth + swap2Stacks[targetIndex].push(sourceItems[2]); // third + swap2Stacks[targetIndex].push(sourceItems[1]); // second + swap2Stacks[targetIndex].push(sourceItems[0]); // top + + // Source stack remains unchanged (no popping/pushing) + + return updateState(state, { + stacks: swap2Stacks, + output: [...state.output, `Copied top 2 pairs of items from ${getFocusedStackName(sourceIndex)} to ${getFocusedStackName(targetIndex)}`] + }); + + default: + return updateState(state, { + output: [...state.output, `Error: Unknown cross-stack operation: ${operation}`] + }); + } +}; + +// Built-in words with documentation +const builtinWords = { + // Stack focus commands + 'focus.red': { + fn: (state) => updateState(state, { + focusedStack: 0, + output: [...state.output, 'Focus set to Red stack (Stack 1)'] + }), + doc: 'Set focus to Red stack (Stack 1)', + stack: '( -- )' + }, + 'focus.1': { + fn: (state) => updateState(state, { + focusedStack: 0, + output: [...state.output, 'Focus set to Red stack (Stack 1)'] + }), + doc: 'Set focus to Red stack (Stack 1)', + stack: '( -- )' + }, + 'focus.teal': { + fn: (state) => updateState(state, { + focusedStack: 1, + output: [...state.output, 'Focus set to Teal stack (Stack 2)'] + }), + doc: 'Set focus to Teal stack (Stack 2)', + stack: '( -- )' + }, + 'focus.2': { + fn: (state) => updateState(state, { + focusedStack: 1, + output: [...state.output, 'Focus set to Teal stack (Stack 2)'] + }), + doc: 'Set focus to Teal stack (Stack 2)', + stack: '( -- )' + }, + 'focus.blue': { + fn: (state) => updateState(state, { + focusedStack: 2, + output: [...state.output, 'Focus set to Blue stack (Stack 3)'] + }), + doc: 'Set focus to Blue stack (Stack 3)', + stack: '( -- )' + }, + 'focus.3': { + fn: (state) => updateState(state, { + focusedStack: 2, + output: [...state.output, 'Focus set to Blue stack (Stack 3)'] + }), + doc: 'Set focus to Blue stack (Stack 3)', + stack: '( -- )' + }, + 'focus.yellow': { + fn: (state) => updateState(state, { + focusedStack: 3, + output: [...state.output, 'Focus set to Yellow stack (Stack 4)'] + }), + doc: 'Set focus to Yellow stack (Stack 4)', + stack: '( -- )' + }, + 'focus.4': { + fn: (state) => updateState(state, { + focusedStack: 3, + output: [...state.output, 'Focus set to Yellow stack (Stack 4)'] + }), + doc: 'Set focus to Yellow stack (Stack 4)', + stack: '( -- )' + }, + 'focus.show': { + fn: (state) => { + const stackNames = ['Red (1)', 'Teal (2)', 'Blue (3)', 'Yellow (4)']; + return updateState(state, { + output: [...state.output, `Currently focused on: ${stackNames[state.focusedStack]}`] + }); + }, + doc: 'Show which stack is currently focused', + stack: '( -- )' + }, + + // Stack manipulation for focused stack + 'dup': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + const stackNames = ['Red (1)', 'Teal (2)', 'Blue (3)', 'Yellow (4)']; + return updateState(state, { + output: [...state.output, `Error: Stack underflow on dup - ${stackNames[state.focusedStack]} is empty. Use numbers or strings to add items first.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + return updateState(state, { + stacks: pushToStack(state.stacks, state.focusedStack, top) + }); + }, + doc: 'Duplicate the top item on the stack', + stack: '( x -- x x )' + }, + + 'swap': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + const stackNames = ['Red (1)', 'Teal (2)', 'Blue (3)', 'Yellow (4)']; + return updateState(state, { + output: [...state.output, `Error: Stack underflow on swap - ${stackNames[state.focusedStack]} needs 2 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const newStacks = state.stacks.map((stack, i) => [...stack]); + const a = newStacks[state.focusedStack].pop(); + const b = newStacks[state.focusedStack].pop(); + newStacks[state.focusedStack].push(a); + newStacks[state.focusedStack].push(b); + return updateState(state, { stacks: newStacks }); + }, + doc: 'Exchange the top two items on the stack', + stack: '( x1 x2 -- x2 x1 )' + }, + + 'drop': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on drop - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to remove.`] + }); + } + const { stacks } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { stacks }); + }, + doc: 'Remove the top item from the stack', + stack: '( x -- )' + }, + + '2drop': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on 2drop - Stack 1 (Red) needs 2 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const { stacks: stacks1 } = popFromStack(state.stacks, state.focusedStack); + const { stacks } = popFromStack(stacks1, 0); + return updateState(state, { stacks }); + }, + doc: 'Remove the top two items from the stack', + stack: '( x1 x2 -- )' + }, + + 'over': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on over - Stack 1 (Red) needs 2 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const second = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 2]; + return updateState(state, { + stacks: pushToStack(state.stacks, state.focusedStack, second) + }); + }, + doc: 'Copy the second item on the stack to the top', + stack: '( x1 x2 -- x1 x2 x1 )' + }, + + '2dup': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on 2dup - Stack 1 (Red) needs 2 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + const second = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 2]; + const newStacks = pushToStack(state.stacks, state.focusedStack, second); + return updateState(state, { + stacks: pushToStack(newStacks, state.focusedStack, top) + }); + }, + doc: 'Duplicate the top two items on the stack', + stack: '( x1 x2 -- x1 x2 x1 x2 )' + }, + + 'rot': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 3) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on rot - Stack 1 (Red) needs 3 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const newStacks = state.stacks.map((stack, i) => [...stack]); + const a = newStacks[state.focusedStack].pop(); + const b = newStacks[state.focusedStack].pop(); + const c = newStacks[state.focusedStack].pop(); + newStacks[state.focusedStack].push(b); + newStacks[state.focusedStack].push(a); + newStacks[state.focusedStack].push(c); + return updateState(state, { stacks: newStacks }); + }, + doc: 'Rotate the top three items on the stack', + stack: '( x1 x2 x3 -- x2 x3 x1 )' + }, + + '-rot': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 3) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on -rot - Stack 1 (Red) needs 3 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const newStacks = state.stacks.map((stack, i) => [...stack]); + const a = newStacks[state.focusedStack].pop(); + const b = newStacks[state.focusedStack].pop(); + const c = newStacks[state.focusedStack].pop(); + newStacks[state.focusedStack].push(a); + newStacks[state.focusedStack].push(c); + newStacks[state.focusedStack].push(b); + return updateState(state, { stacks: newStacks }); + }, + doc: 'Rotate the top three items on the stack (reverse of rot)', + stack: '( x1 x2 x3 -- x3 x1 x2 )' + }, + + // Cross-stack operations + 'dup.stacks': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on dup.stacks - ${getFocusedStackName(state.focusedStack)} is empty. Add an item first.`] + }); + } + return updateState(state, { + crossStackInProgress: true, + crossStackOperation: 'dup', + crossStackData: { top: state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1] }, + output: [...state.output, `Enter destination stack (1-4) to duplicate to:`] + }); + }, + doc: 'Duplicate top item from focused stack to another stack', + stack: '( x -- x x ) (interactive)' + }, + + 'over.stacks': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on over.stacks - ${getFocusedStackName(state.focusedStack)} needs 2 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const second = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 2]; + return updateState(state, { + crossStackInProgress: true, + crossStackOperation: 'over', + crossStackData: { second }, + output: [...state.output, `Enter destination stack (1-4) to copy second item to:`] + }); + }, + doc: 'Copy second item from focused stack to another stack', + stack: '( x1 x2 -- x1 x2 x1 ) (interactive)' + }, + + 'swap.stacks': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on swap.stacks - ${getFocusedStackName(state.focusedStack)} needs 2 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + const second = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 2]; + return updateState(state, { + crossStackInProgress: true, + crossStackOperation: 'swap', + crossStackData: { top, second }, + output: [...state.output, `Enter target stack (1-4) to swap with:`] + }); + }, + doc: 'Swap top items between focused stack and another stack', + stack: '( x1 x2 -- x2 x1 ) (interactive)' + }, + + 'nip.stacks': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on nip.stacks - ${getFocusedStackName(state.focusedStack)} needs 2 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + const second = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 2]; + return updateState(state, { + crossStackInProgress: true, + crossStackOperation: 'nip', + crossStackData: { top, second }, + output: [...state.output, `Enter destination stack (1-4) to move second item to:`] + }); + }, + doc: 'Move second item from focused stack to another stack (remove from source)', + stack: '( x1 x2 -- x1 ) (interactive)' + }, + + 'tuck.stacks': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on tuck.stacks - ${getFocusedStackName(state.focusedStack)} needs 2 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + const second = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 2]; + return updateState(state, { + crossStackInProgress: true, + crossStackOperation: 'tuck', + crossStackData: { top, second }, + output: [...state.output, `Enter destination stack (1-4) to tuck top item under second item:`] + }); + }, + doc: 'Tuck top item under second item on another stack', + stack: '( x1 x2 -- x2 x1 x2 ) (interactive)' + }, + + 'rot.stacks': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 3) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on rot.stacks - ${getFocusedStackName(state.focusedStack)} needs 3 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const first = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 3]; + const second = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 2]; + const third = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + return updateState(state, { + crossStackInProgress: true, + crossStackOperation: 'rot', + crossStackData: { first, second, third }, + output: [...state.output, `Enter destination stack (1-4) to rotate with:`] + }); + }, + doc: 'Rotate top 3 items between focused stack and another stack', + stack: '( x1 x2 x3 -- x2 x3 x1 ) (interactive)' + }, + + '2dup.stacks': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on 2dup.stacks - ${getFocusedStackName(state.focusedStack)} needs 2 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + const second = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 2]; + return updateState(state, { + crossStackInProgress: true, + crossStackOperation: '2dup', + crossStackData: { top, second }, + output: [...state.output, `Enter destination stack (1-4) to duplicate top 2 items to:`] + }); + }, + doc: 'Duplicate top 2 items from focused stack to another stack', + stack: '( x1 x2 -- x1 x2 x1 x2 ) (interactive)' + }, + + '2over.stacks': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 4) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on 2over.stacks - ${getFocusedStackName(state.focusedStack)} needs 4 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + return updateState(state, { + crossStackInProgress: true, + crossStackOperation: '2over', + crossStackData: {}, + output: [...state.output, `Enter destination stack (1-4) to copy second pair of items to:`] + }); + }, + doc: 'Copy second pair of items from focused stack to another stack', + stack: '( x1 x2 x3 x4 -- x1 x2 x3 x4 x1 x2 ) (interactive)' + }, + + '2swap.stacks': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 4) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on 2swap.stacks - ${getFocusedStackName(state.focusedStack)} needs 4 items, but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const first = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 4]; + const second = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 3]; + const third = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 2]; + const fourth = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + return updateState(state, { + crossStackInProgress: true, + crossStackOperation: '2swap', + crossStackData: { first, second, third, fourth }, + output: [...state.output, `Enter destination stack (1-4) to swap top 2 pairs with:`] + }); + }, + doc: 'Swap top 2 pairs of items between focused stack and another stack', + stack: '( x1 x2 x3 x4 -- x3 x4 x1 x2 ) (interactive)' + }, + + // Arithmetic operations on stack 1 + '+': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on + - ${getFocusedStackName(state.focusedStack)} needs 2 numbers, but has ${state.stacks[state.focusedStack].length}. Add more numbers first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, a + b) + }); + }, + doc: 'Add the top two numbers on the stack', + stack: '( n1 n2 -- n3 )' + }, + + '-': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on - - Stack 1 (Red) needs 2 numbers, but has ${state.stacks[state.focusedStack].length}. Add more numbers first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, a - b) + }); + }, + doc: 'Subtract the top number from the second number on the stack', + stack: '( n1 n2 -- n3 )' + }, + + '*': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on * - Stack 1 (Red) needs 2 numbers, but has ${state.stacks[state.focusedStack].length}. Add more numbers first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, a * b) + }); + }, + doc: 'Multiply the top two numbers on the stack', + stack: '( n1 n2 -- n3 )' + }, + + '/': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on / - Stack 1 (Red) needs 2 numbers, but has ${state.stacks[state.focusedStack].length}. Add more numbers first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + if (b === 0) { + return updateState(state, { + stacks: pushToStack(pushToStack(stacks2, state.focusedStack, a), state.focusedStack, b), + output: [...state.output, 'Error: Division by zero - Cannot divide by zero. Check your divisor before dividing.'] + }); + } + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, Math.floor(a / b)) + }); + }, + doc: 'Divide the second number by the top number on the stack (integer division)', + stack: '( n1 n2 -- n3 )' + }, + + 'mod': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on mod - Stack 1 (Red) needs 2 numbers, but has ${state.stacks[state.focusedStack].length}. Add more numbers first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + if (b === 0) { + return updateState(state, { + stacks: pushToStack(pushToStack(stacks2, state.focusedStack, a), state.focusedStack, b), + output: [...state.output, 'Error: Modulo by zero - Cannot calculate remainder when dividing by zero. Check your divisor first.'] + }); + } + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, a % b) + }); + }, + doc: 'Return the remainder of dividing the second number by the top number', + stack: '( n1 n2 -- n3 )' + }, + + 'abs': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on abs - Stack 1 (Red) is empty. Add a number first.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks, state.focusedStack, Math.abs(value)) + }); + }, + doc: 'Return the absolute value of the top number on the stack', + stack: '( n -- |n| )' + }, + + 'negate': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on negate - ${getFocusedStackName(state.focusedStack)} is empty. Add a number first.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks, state.focusedStack, -value) + }); + }, + doc: 'Return the negative of the top number on the stack', + stack: '( n -- -n )' + }, + + 'min': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on min - Stack 1 (Red) needs 2 numbers, but has ${state.stacks[state.focusedStack].length}. Add more numbers first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, Math.min(a, b)) + }); + }, + doc: 'Return the smaller of the top two numbers on the stack', + stack: '( n1 n2 -- n3 )' + }, + + 'max': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on max - Stack 1 (Red) needs 2 numbers, but has ${state.stacks[state.focusedStack].length}. Add more numbers first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, Math.max(a, b)) + }); + }, + doc: 'Return the larger of the top two numbers on the stack', + stack: '( n1 n2 -- n3 )' + }, + + // Comparison and logic + '=': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on = - Stack 1 (Red) needs 2 values, but has ${state.stacks[state.focusedStack].length}. Add more values first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, a === b ? -1 : 0) + }); + }, + doc: 'Return true (-1) if the top two numbers are equal, false (0) otherwise', + stack: '( n1 n2 -- flag )' + }, + + '<': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on < - Stack 1 (Red) needs 2 values, but has ${state.stacks[state.focusedStack].length}. Add more values first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, a < b ? -1 : 0) + }); + }, + doc: 'Return true (-1) if the second number is less than the top number, false (0) otherwise', + stack: '( n1 n2 -- flag )' + }, + + '>': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on > - Stack 1 (Red) needs 2 values, but has ${state.stacks[state.focusedStack].length}. Add more values first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, a > b ? -1 : 0) + }); + }, + doc: 'Return true (-1) if the second number is greater than the top number, false (0) otherwise', + stack: '( n1 n2 -- flag )' + }, + + 'and': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on and - Stack 1 (Red) needs 2 values, but has ${state.stacks[state.focusedStack].length}. Add more values first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, a && b ? -1 : 0) + }); + }, + doc: 'Return true (-1) if both top two values are true, false (0) otherwise', + stack: '( x1 x2 -- flag )' + }, + + 'or': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on or - Stack 1 (Red) needs 2 values, but has ${state.stacks[state.focusedStack].length}. Add more values first.`] + }); + } + const { stacks: stacks1, value: b } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: a } = popFromStack(stacks1, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, a || b ? -1 : 0) + }); + }, + doc: 'Return true (-1) if either of the top two values is true, false (0) otherwise', + stack: '( x1 x2 -- flag )' + }, + + 'not': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on not - Stack 1 (Red) is empty. Add a value first.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks, state.focusedStack, value ? 0 : -1) + }); + }, + doc: 'Return true (-1) if the top value is false, false (0) if it is true', + stack: '( x -- flag )' + }, + + // Stack inspection + '.s': { + fn: (state) => { + const stackStr = state.stacks[state.focusedStack].length === 0 ? 'empty' : state.stacks[0].join(' '); + return updateState(state, { + output: [...state.output, `Stack 1 (red): ${stackStr}`] + }); + }, + doc: 'Display the contents of the red stack (non-destructive)', + stack: '( -- )' + }, + + 'depth': { + fn: (state) => { + return updateState(state, { + stacks: pushToStack(state.stacks, state.focusedStack, state.stacks[state.focusedStack].length) + }); + }, + doc: 'Push the number of items on the red stack', + stack: '( -- n )' + }, + + '.': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on . - Stack 1 (Red) is empty. Nothing to print.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks, + output: [...state.output, value.toString()] + }); + }, + doc: 'Pop and print the top item from the red stack', + stack: '( x -- )' + }, + + // Multi-stack operations + 'move.red': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on move.red - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks, 0, value) + }); + }, + doc: 'Move top item from focused stack to red stack (removes from focused stack)', + stack: '( x -- )' + }, + + 'move.1': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on move.1 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks, 0, value) + }); + }, + doc: 'Move top item from focused stack to red stack (Stack 1) (removes from focused stack)', + stack: '( x -- )' + }, + + 'move.teal': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on move.teal - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks, 1, value) + }); + }, + doc: 'Move top item from focused stack to teal stack (removes from focused stack)', + stack: '( x -- )' + }, + + 'move.2': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on move.2 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks, 1, value) + }); + }, + doc: 'Move top item from focused stack to teal stack (Stack 2) (removes from focused stack)', + stack: '( x -- )' + }, + + 'move.blue': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on move.blue - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks, 2, value) + }); + }, + doc: 'Move top item from focused stack to blue stack (removes from focused stack)', + stack: '( x -- )' + }, + + 'move.3': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on move.3 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks, 2, value) + }); + }, + doc: 'Move top item from focused stack to blue stack (Stack 3) (removes from focused stack)', + stack: '( x -- )' + }, + + 'move.yellow': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on move.yellow - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks, 3, value) + }); + }, + doc: 'Move top item from focused stack to yellow stack (removes from focused stack)', + stack: '( x -- )' + }, + + 'move.4': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on move.4 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to move.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + return updateState(state, { + stacks: pushToStack(stacks, 3, value) + }); + }, + doc: 'Move top item from focused stack to yellow stack (Stack 4) (removes from focused stack)', + stack: '( x -- )' + }, + + // Copy operations (keep item in source stack) + 'copy.red': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on copy.red - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + return updateState(state, { + stacks: pushToStack(state.stacks, 0, top) + }); + }, + doc: 'Copy top item from focused stack to red stack (keeps item on focused stack)', + stack: '( x -- x )' + }, + + 'copy.1': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on copy.1 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + return updateState(state, { + stacks: pushToStack(state.stacks, 0, top) + }); + }, + doc: 'Copy top item from focused stack to red stack (Stack 1) (keeps item on focused stack)', + stack: '( x -- x )' + }, + + 'copy.teal': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on copy.teal - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + return updateState(state, { + stacks: pushToStack(state.stacks, 1, top) + }); + }, + doc: 'Copy top item from focused stack to teal stack (keeps item on focused stack)', + stack: '( x -- x )' + }, + + 'copy.2': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on copy.2 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + return updateState(state, { + stacks: pushToStack(state.stacks, 1, top) + }); + }, + doc: 'Copy top item from focused stack to teal stack (Stack 2) (keeps item on focused stack)', + stack: '( x -- x )' + }, + + 'copy.blue': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on copy.blue - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + return updateState(state, { + stacks: pushToStack(state.stacks, 2, top) + }); + }, + doc: 'Copy top item from focused stack to blue stack (keeps item on focused stack)', + stack: '( x -- x )' + }, + + 'copy.3': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on copy.3 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + return updateState(state, { + stacks: pushToStack(state.stacks, 2, top) + }); + }, + doc: 'Copy top item from focused stack to blue stack (Stack 3) (keeps item on focused stack)', + stack: '( x -- x )' + }, + + 'copy.yellow': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on copy.yellow - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + return updateState(state, { + stacks: pushToStack(state.stacks, 3, top) + }); + }, + doc: 'Copy top item from focused stack to yellow stack (keeps item on focused stack)', + stack: '( x -- x )' + }, + + 'copy.4': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on copy.4 - ${getFocusedStackName(state.focusedStack)} is empty. Nothing to copy.`] + }); + } + const top = state.stacks[state.focusedStack][state.stacks[state.focusedStack].length - 1]; + return updateState(state, { + stacks: pushToStack(state.stacks, 3, top) + }); + }, + doc: 'Copy top item from focused stack to yellow stack (Stack 4) (keeps item on focused stack)', + stack: '( x -- x )' + }, + + 'pop.red': { + fn: (state) => popAndPrint(state, 0), + doc: 'Pop and print top item from red stack', + stack: '( x -- )' + }, + + 'pop.1': { + fn: (state) => popAndPrint(state, 0), + doc: 'Pop and print top item from red stack (Stack 1)', + stack: '( x -- )' + }, + + 'pop.teal': { + fn: (state) => popAndPrint(state, 1), + doc: 'Pop and print top item from teal stack', + stack: '( x -- )' + }, + + 'pop.2': { + fn: (state) => popAndPrint(state, 1), + doc: 'Pop and print top item from teal stack (Stack 2)', + stack: '( x -- )' + }, + + 'pop.blue': { + fn: (state) => popAndPrint(state, 2), + doc: 'Pop and print top item from blue stack', + stack: '( x -- )' + }, + + 'pop.3': { + fn: (state) => popAndPrint(state, 2), + doc: 'Pop and print top item from blue stack (Stack 3)', + stack: '( x -- )' + }, + + 'pop.yellow': { + fn: (state) => popAndPrint(state, 3), + doc: 'Pop and print top item from yellow stack', + stack: '( x -- )' + }, + + 'pop.4': { + fn: (state) => popAndPrint(state, 3), + doc: 'Pop and print top item from yellow stack (Stack 4)', + stack: '( x -- )' + }, + + // Move operations + 'move': { + fn: (state) => { + return updateState(state, { + moveInProgress: true, + moveFromStack: null, + output: [...state.output, 'Enter source stack (1-4) to move from:'] + }); + }, + doc: 'Move top item from one stack to another (interactive: source, then destination)', + stack: '( -- ) (interactive)' + }, + + // Utility words + 'clear': { + fn: (state) => updateState(state, { + stacks: [[], [], [], []], + output: [...state.output, 'All stacks cleared'] + }), + doc: 'Clear all four stacks', + stack: '( -- )' + }, + + 'clear.all': { + fn: (state) => updateState(state, { + stacks: [[], [], [], []], + output: [...state.output, 'All stacks cleared'] + }), + doc: 'Clear all four stacks (alias for clear)', + stack: '( -- )' + }, + + 'clear.focused': { + fn: (state) => { + const stackNames = ['Red (1)', 'Teal (2)', 'Blue (3)', 'Yellow (4)']; + const newStacks = state.stacks.map((stack, i) => + i === state.focusedStack ? [] : stack + ); + return updateState(state, { + stacks: newStacks, + output: [...state.output, `${stackNames[state.focusedStack]} cleared`] + }); + }, + doc: 'Clear only the currently focused stack', + stack: '( -- )' + }, + + // String operations + '."': { + fn: (state) => { + return updateState(state, { + stringMode: true, + currentString: '', + stringPushMode: false + }); + }, + doc: 'Begin a string literal that will be printed to output', + stack: '( -- )' + }, + + 's"': { + fn: (state) => { + return updateState(state, { + stringMode: true, + currentString: '', + stringPushMode: true + }); + }, + doc: 'Begin a string literal that will be pushed to the stack', + stack: '( -- )' + }, + + 'type': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on type - Stack 1 (Red) needs 2 items (address and length), but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const { stacks: stacks1, value: length } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: string } = popFromStack(stacks1, 0); + return updateState(state, { + stacks: stacks2, + output: [...state.output, string.toString()] + }); + }, + doc: 'Print a string from the stack (takes length and string address)', + stack: '( addr len -- )' + }, + + 'count': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on count - Stack 1 (Red) is empty. Add a string first.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + if (typeof value === 'string') { + const newStacks = pushToStack(stacks, state.focusedStack, value.length); + return updateState(state, { + stacks: pushToStack(newStacks, state.focusedStack, value) + }); + } else { + const newStacks = pushToStack(stacks, state.focusedStack, 0); + return updateState(state, { + stacks: pushToStack(newStacks, state.focusedStack, '') + }); + } + }, + doc: 'Extract string info from counted string (returns length and address)', + stack: '( c-addr -- c-addr u )' + }, + + 'char+': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on char+ - Stack 1 (Red) needs 2 items (string and offset), but has ${state.stacks[state.focusedStack].length}. Add more items first.`] + }); + } + const { stacks: stacks1, value: offset } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: string } = popFromStack(stacks1, 0); + if (typeof string === 'string') { + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, string + String.fromCharCode(offset)) + }); + } else { + return updateState(state, { + stacks: pushToStack(pushToStack(stacks2, state.focusedStack, string), 0, offset), + output: [...state.output, `Error: char+ requires string on stack - Got ${typeof string}, expected string. Use s" to create strings.`] + }); + } + }, + doc: 'Add a character to a string using ASCII offset', + stack: '( c-addr1 char -- c-addr2 )' + }, + + 'strlen': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on strlen - Stack 1 (Red) is empty. Add a string first.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + if (typeof value === 'string') { + return updateState(state, { + stacks: pushToStack(stacks, state.focusedStack, value.length) + }); + } else { + return updateState(state, { + stacks: pushToStack(stacks, state.focusedStack, 0), + output: [...state.output, `Error: strlen requires string on stack - Got ${typeof value}, expected string. Use s" to create strings.`] + }); + } + }, + doc: 'Get the length of a string on the stack', + stack: '( str -- len )' + }, + + 'strcat': { + fn: (state) => { + if (state.stacks[state.focusedStack].length < 2) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on strcat - Stack 1 (Red) needs 2 strings, but has ${state.stacks[state.focusedStack].length}. Add more strings first.`] + }); + } + const { stacks: stacks1, value: str2 } = popFromStack(state.stacks, state.focusedStack); + const { stacks: stacks2, value: str1 } = popFromStack(stacks1, 0); + if (typeof str1 === 'string' && typeof str2 === 'string') { + return updateState(state, { + stacks: pushToStack(stacks2, state.focusedStack, str1 + str2) + }); + } else { + return updateState(state, { + stacks: pushToStack(pushToStack(stacks2, state.focusedStack, str1), 0, str2), + output: [...state.output, `Error: strcat requires two strings - Got ${typeof str1} and ${typeof str2}. Use s" to create strings.`] + }); + } + }, + doc: 'Concatenate two strings from the stack', + stack: '( str1 str2 -- str3 )' + }, + + // Control flow + 'if': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on if - Stack 1 (Red) is empty. Add a condition value first.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + if (value === 0) { + // Skip until THEN or ELSE + return updateState(state, { + stacks, + skipMode: true, + skipCount: 0 + }); + } + return updateState(state, { + stacks + }); + }, + doc: 'Begin conditional execution - if top of stack is false (0), skip to THEN', + stack: '( flag -- )' + }, + + 'else': { + fn: (state) => { + if (state.skipMode) { + return updateState(state, { + skipCount: state.skipCount + 1 + }); + } + // Skip until THEN + return updateState(state, { + skipMode: true, + skipCount: 0 + }); + }, + doc: 'Begin alternative branch in conditional execution', + stack: '( -- )' + }, + + 'then': { + fn: (state) => { + if (state.skipMode && state.skipCount > 0) { + return updateState(state, { + skipCount: state.skipCount - 1 + }); + } else if (state.skipMode) { + return updateState(state, { + skipMode: false, + skipCount: 0 + }); + } + return state; + }, + doc: 'End conditional execution block', + stack: '( -- )' + }, + + 'begin': { + fn: (state) => { + return updateState(state, { + loopStart: state.output.length + }); + }, + doc: 'Mark the beginning of a loop', + stack: '( -- )' + }, + + 'until': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on until - Stack 1 (Red) is empty. Add a condition value first.`] + }); + } + const { stacks, value } = popFromStack(state.stacks, state.focusedStack); + if (value === 0) { + // Loop back to BEGIN + return updateState(state, { + stacks, + loopBack: true + }); + } + return updateState(state, { + stacks + }); + }, + doc: 'End a loop - if top of stack is false (0), loop back to BEGIN', + stack: '( flag -- )' + }, + + // Help and documentation + 'help': { + fn: (state) => { + const builtinWordNames = Object.keys(builtinWords); + const userWords = Array.from(state.dictionary.keys()); + const allWords = [...builtinWordNames, ...userWords]; + + const builtinList = builtinWordNames.map(name => { + const word = builtinWords[name]; + return `${name} ${word.stack} - ${word.doc}`; + }); + + const userList = userWords.length > 0 ? userWords.join(' ') : 'none'; + + return updateState(state, { + output: [ + ...state.output, + '=== 4-Stack Forth Interpreter Help ===', + '', + 'Built-in words:', + ...builtinList, + '', + 'User defined words: ' + userList, + '', + 'Total words: ' + allWords.length, + '', + 'Use "doc <word>" to get detailed help for a specific word', + 'Use "words" to see just the word names' + ] + }); + }, + doc: 'Display comprehensive help information for all available words', + stack: '( -- )' + }, + + 'doc': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on doc - Stack 1 (Red) is empty. Add a word name first (e.g., s" dup" doc).`] + }); + } + const { stacks, value: wordName } = popFromStack(state.stacks, state.focusedStack); + + // Check built-in words first + if (builtinWords[wordName]) { + const word = builtinWords[wordName]; + return updateState(state, { + stacks, + output: [ + ...state.output, + `=== ${wordName} ===`, + `Stack effect: ${word.stack}`, + `Description: ${word.doc}`, + `Type: Built-in word` + ] + }); + } + + // Check user-defined words + if (state.dictionary.has(wordName)) { + const definition = state.dictionary.get(wordName); + return updateState(state, { + stacks, + output: [ + ...state.output, + `=== ${wordName} ===`, + `Definition: ${definition.join(' ')}`, + `Type: User-defined word` + ] + }); + } + + return updateState(state, { + stacks, + output: [...state.output, `Word '${wordName}' not found`] + }); + }, + doc: 'Display documentation for a specific word', + stack: '( "word" -- )' + }, + + 'words': { + fn: (state) => { + const builtinWordNames = Object.keys(builtinWords); + const userWords = Array.from(state.dictionary.keys()); + const allWords = [...builtinWordNames, ...userWords]; + + const builtinList = builtinWordNames.join(' '); + const userList = userWords.length > 0 ? userWords.join(' ') : 'none'; + + return updateState(state, { + output: [ + ...state.output, + 'Built-in words: ' + builtinList, + 'User defined words: ' + userWords.join(' '), + 'Total words: ' + allWords.length + ] + }); + }, + doc: 'List all available words (built-in and user-defined)', + stack: '( -- )' + } +}; + +// Help and documentation commands (defined outside builtinWords to avoid circular reference) +const helpCommands = { + 'help': { + fn: (state) => { + const builtinWordNames = Object.keys(builtinWords); + const userWords = Array.from(state.dictionary.keys()); + const allWords = [...builtinWordNames, ...userWords]; + + const builtinList = builtinWordNames.map(name => { + const word = builtinWords[name]; + return `${name} ${word.stack} - ${word.doc}`; + }); + + const userList = userWords.length > 0 ? userWords.join(' ') : 'none'; + + return updateState(state, { + output: [ + ...state.output, + '=== 4-Stack Forth Interpreter Help ===', + '', + 'Built-in words:', + ...builtinList, + '', + 'User defined words: ' + userList, + '', + 'Total words: ' + allWords.length, + '', + 'Use "doc <word>" to get detailed help for a specific word', + 'Use "words" to see just the word names' + ] + }); + }, + doc: 'Display comprehensive help information for all available words', + stack: '( -- )' + }, + + 'doc': { + fn: (state) => { + if (state.stacks[state.focusedStack].length === 0) { + return updateState(state, { + output: [...state.output, `Error: Stack underflow on doc - Stack 1 (Red) is empty. Add a word name first (e.g., s" dup" doc).`] + }); + } + const { stacks, value: wordName } = popFromStack(state.stacks, state.focusedStack); + + // Check built-in words first + if (builtinWords[wordName]) { + const word = builtinWords[wordName]; + return updateState(state, { + stacks, + output: [ + ...state.output, + `=== ${wordName} ===`, + `Stack effect: ${word.stack}`, + `Description: ${word.doc}`, + `Type: Built-in word` + ] + }); + } + + // Check user-defined words + if (state.dictionary.has(wordName)) { + const definition = state.dictionary.get(wordName); + return updateState(state, { + stacks, + output: [ + ...state.output, + `=== ${wordName} ===`, + `Definition: ${definition.join(' ')}`, + `Type: User-defined word` + ] + }); + } + + return updateState(state, { + stacks, + output: [...state.output, `Word '${wordName}' not found`] + }); + }, + doc: 'Display documentation for a specific word', + stack: '( "word" -- )' + } +}; + +// Parse and execute Forth input +const parseAndExecute = (state, input) => { + const tokens = input.trim().split(/\s+/).filter(token => token.length > 0); + return tokens.reduce(executeToken, state); +}; + +// Execute a single token +const executeToken = (state, token) => { + // Handle string mode + if (state.stringMode) { + // Check if this token contains the closing quote + const quoteIndex = token.indexOf('"'); + if (quoteIndex !== -1) { + // Token contains closing quote + const beforeQuote = token.substring(0, quoteIndex); + const afterQuote = token.substring(quoteIndex + 1); + + // Add the part before the quote to the string + const finalString = state.currentString + (state.currentString ? ' ' : '') + beforeQuote; + + // End string mode and handle based on mode + let newState; + if (state.stringPushMode) { + // Push mode: add string to stack + newState = updateState(state, { + stringMode: false, + currentString: '', + stringPushMode: false, + stacks: pushToStack(state.stacks, state.focusedStack, finalString) + }); + } else { + // Print mode: add to output + newState = updateState(state, { + stringMode: false, + currentString: '', + stringPushMode: false, + output: [...state.output, finalString] + }); + } + + // If there's content after the quote, process it + if (afterQuote.trim()) { + newState = ForthInterpreter.parseAndExecute(newState, afterQuote); + } + + return newState; + } else { + // Add to current string + return updateState(state, { + currentString: state.currentString + (state.currentString ? ' ' : '') + token + }); + } + } + + // Handle skip mode (for control flow) + if (state.skipMode) { + if (token === 'if' || token === 'begin') { + return updateState(state, { + skipCount: state.skipCount + 1 + }); + } else if (token === 'then' || token === 'until') { + if (state.skipCount > 0) { + return updateState(state, { + skipCount: state.skipCount - 1 + }); + } else { + return updateState(state, { + skipMode: false, + skipCount: 0 + }); + } + } else if (token === 'else') { + if (state.skipCount === 0) { + // Switch to skipping ELSE branch + return updateState(state, { + skipMode: true, + skipCount: 0 + }); + } + } + // Skip this token + return state; + } + + // Handle move operation state machine + if (state.moveInProgress) { + if (state.moveFromStack === null) { + // Expecting source stack number + const from = parseInt(token); + if (isNaN(from) || from < 1 || from > 4) { + return updateState(state, { + moveInProgress: false, + moveFromStack: null, + output: [...state.output, `Error: Invalid source stack ${from} - Must be 1, 2, 3, or 4.`] + }); + } + if (state.stacks[from - 1].length === 0) { + return updateState(state, { + moveInProgress: false, + moveFromStack: null, + output: [...state.output, `Error: Stack ${from} is empty - Nothing to move. Add items to stack ${from} first.`] + }); + } + return updateState(state, { + moveInProgress: true, + moveFromStack: from - 1, // Convert to 0-based index + output: [...state.output, `Moving from stack ${from}. Enter destination stack (1-4):`] + }); + } else { + // Expecting destination stack number + const to = parseInt(token); + if (isNaN(to) || to < 1 || to > 4) { + return updateState(state, { + moveInProgress: false, + moveFromStack: null, + output: [...state.output, `Error: Invalid destination stack ${to} - Must be 1, 2, 3, or 4.`] + }); + } + const toIndex = to - 1; // Convert to 0-based index + const fromIndex = state.moveFromStack; + + // Reset move state + const newState = updateState(state, { + moveInProgress: false, + moveFromStack: null + }); + + // Perform the move + const { stacks, value } = popFromStack(newState.stacks, fromIndex); + return updateState(newState, { + stacks: pushToStack(stacks, toIndex, value), + output: [...newState.output, `Moved ${value} from stack ${fromIndex + 1} to stack ${toIndex + 1}`] + }); + } + } + + // Handle cross-stack operations state machine + if (state.crossStackInProgress) { + if (state.crossStackOperation === null) { + // This shouldn't happen, but handle gracefully + return updateState(state, { + crossStackInProgress: false, + crossStackOperation: null, + crossStackData: null, + output: [...state.output, 'Error: Cross-stack operation state corrupted'] + }); + } + + // Expecting target stack number + const target = parseInt(token); + if (isNaN(target) || target < 1 || target > 4) { + return updateState(state, { + crossStackInProgress: false, + crossStackOperation: null, + crossStackData: null, + output: [...state.output, `Error: Invalid target stack ${target} - Must be 1, 2, 3, or 4.`] + }); + } + + const targetIndex = target - 1; // Convert to 0-based index + const sourceIndex = state.focusedStack; + + // Execute the cross-stack operation (don't clear state yet) + const result = executeCrossStackOperation(state, sourceIndex, targetIndex); + + // Clear cross-stack state after execution + return updateState(result, { + crossStackInProgress: false, + crossStackOperation: null, + crossStackData: null + }); + } + + // Handle word definition compilation + if (state.compilingWord !== null) { + if (token === ';') { + const newDictionary = new Map(state.dictionary); + newDictionary.set(state.compilingWord, [...state.compilingDefinition]); + return updateState(state, { + dictionary: newDictionary, + compilingWord: null, + compilingDefinition: [], + output: [...state.output, `Word '${state.compilingWord}' defined`] + }); + } + + // If we're expecting a name, capture it + if (state.compilingWord === 'EXPECTING_NAME') { + return updateState(state, { + compilingWord: token, + compilingDefinition: [] + }); + } + + // Otherwise, add to definition + return updateState(state, { + compilingDefinition: [...state.compilingDefinition, token] + }); + } + + // Handle word definition start + if (token === ':') { + return updateState(state, { + compilingWord: 'EXPECTING_NAME', + compilingDefinition: [] + }); + } + + // Check if it's a built-in word + if (builtinWords[token]) { + return builtinWords[token].fn(state); + } + + // Check if it's a user-defined word + if (state.dictionary.has(token)) { + const definition = state.dictionary.get(token); + return definition.reduce(executeToken, state); + } + + // Check if it's a number + const num = parseFloat(token); + if (!isNaN(num)) { + return updateState(state, { + stacks: pushToStack(state.stacks, state.focusedStack, num) + }); + } + + + + // Check if it's a cross-stack operation + if (token.endsWith('.stacks')) { + const baseOperation = token.replace('.stacks', ''); + if (builtinWords[token]) { + return builtinWords[token].fn(state); + } + } + + // Unknown token + return updateState(state, { + output: [...state.output, `Error: Unknown word '${token}' - Use 'help' to see all available words, or 'doc <word>' for specific help.`] + }); +}; + +// Export for use in other modules or testing +if (typeof module !== 'undefined' && module.exports) { + module.exports = { + createInitialState, + parseAndExecute, + executeToken, + builtinWords, + updateState, + pushToStack, + popFromStack + }; +} else if (typeof window !== 'undefined') { + // Browser environment + window.ForthInterpreter = { + createInitialState, + parseAndExecute, + executeToken, + builtinWords, + updateState, + pushToStack, + popFromStack + }; +} diff --git a/forth/foreforthfourth/index.html b/forth/foreforthfourth/index.html new file mode 100644 index 0000000..a4400f3 --- /dev/null +++ b/forth/foreforthfourth/index.html @@ -0,0 +1,381 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>4-Stack Forth</title> + <style> + body { + font-family: 'Courier New', monospace; + margin: 0; + padding: 10px; + background-color: #000; + color: #fff; + min-height: 100vh; + display: flex; + flex-direction: column; + } + + .container { + max-width: 100%; + flex: 1; + display: flex; + flex-direction: column; + } + + h1 { + font-size: 18px; + margin: 0 0 10px 0; + text-align: center; + } + + .help { + background-color: #111; + border: 1px solid #333; + padding: 10px; + margin-bottom: 10px; + font-size: 12px; + line-height: 1.3; + } + + .help h2 { + margin: 0 0 5px 0; + font-size: 13px; + } + + .help p { + margin: 0; + } + + .stacks-container { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; + margin-bottom: 10px; + flex: 1; + min-height: 200px; + } + + .stack { + border: 2px solid; + padding: 8px; + display: flex; + flex-direction: column; + background-color: #111; + position: relative; + transition: all 0.3s ease; + } + + .stack.focused { + border-width: 4px; + box-shadow: 0 0 15px rgba(255, 255, 255, 0.3); + transform: scale(1.02); + } + + .stack-1 { border-color: #ff6b6b; } + .stack-2 { border-color: #4ecdc4; } + .stack-3 { border-color: #45b7d1; } + .stack-4 { border-color: #f9ca24; } + + .stack h3 { + margin: 0 0 8px 0; + text-align: center; + font-size: 12px; + color: #fff; + } + + .stack-items { + flex: 1; + display: flex; + flex-direction: column-reverse; + gap: 2px; + align-items: stretch; + } + + .stack-item { + background-color: #222; + padding: 4px 6px; + text-align: center; + border: 1px solid #555; + font-size: 12px; + word-break: break-all; + } + + .input-section { + margin-bottom: 10px; + } + + .input-container { + display: flex; + gap: 5px; + margin-bottom: 8px; + } + + #forth-input { + flex: 1; + padding: 8px; + font-family: 'Courier New', monospace; + font-size: 14px; + border: 1px solid #555; + background-color: #111; + color: #fff; + } + + #forth-input:focus { + outline: none; + border-color: #fff; + } + + button { + padding: 8px 12px; + font-family: 'Courier New', monospace; + font-size: 12px; + border: 1px solid #fff; + background-color: #000; + color: #fff; + cursor: pointer; + } + + button:hover, button:active { + background-color: #fff; + color: #000; + } + + .output { + background-color: #111; + border: 1px solid #555; + padding: 8px; + height: 300px; + overflow-y: auto; + font-family: 'Courier New', monospace; + font-size: 12px; + line-height: 1.3; + } + + /* Semantic section styling */ + section { + margin-bottom: 10px; + } + + section h2 { + margin: 0 0 8px 0; + font-size: 14px; + color: #fff; + } + + .output-section h2 { + margin-bottom: 5px; + } + + .error { + color: #fff; + } + + .success { + color: #fff; + } + + /* Screen reader only content */ + .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + } + + /* Mobile optimizations */ + @media (max-width: 768px) { + body { padding: 5px; } + + h1 { font-size: 16px; } + + .help { + font-size: 11px; + padding: 8px; + } + + .help h2 { + font-size: 12px; + } + + .stacks-container { + gap: 4px; + min-height: 150px; + } + + .stack { + padding: 6px; + } + + .stack h3 { + font-size: 11px; + margin-bottom: 6px; + } + + .stack-item { + font-size: 11px; + padding: 3px 4px; + } + + #forth-input { + font-size: 16px; /* Prevents zoom on iOS */ + padding: 6px; + } + + button { + font-size: 11px; + padding: 6px 10px; + } + + .output { + height: 80px; + font-size: 11px; + padding: 6px; + } + } + + @media (max-width: 480px) { + .stacks-container { + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(2, 1fr); + } + + .input-container { + flex-direction: column; + } + + button { + width: 100%; + } + } + </style> +</head> +<body> + <header> + <h1>4-Stack Forth</h1> + </header> + + <main class="container"> + <section class="help" aria-label="Quick help"> + <h2>Quick Help</h2> + <p>Type <strong>help</strong> to see all available words with documentation and stack effects. Add a word to the stack like <code>s" dup"</code> and then run <code>doc</code> to see documentation for that word.</p> + <p><strong>Stack Focus:</strong> Use <code>focus.red</code>, <code>focus.teal</code>, <code>focus.blue</code>, or <code>focus.yellow</code> (or <code>focus.1</code>, <code>focus.2</code>, <code>focus.3</code>, <code>focus.4</code>) to switch which stack operations target. Currently focused: <span id="current-focus">Red (1)</span></p> + </section> + + <section class="stacks-container" aria-label="Data stacks"> + <h2 class="sr-only">Data Stacks</h2> + <div class="stack stack-1" role="region" aria-label="Stack 1 (Red)" id="stack-1"> + <h3>Stack 1 (Red)</h3> + <div class="stack-items" id="stack-1-items" aria-live="polite"></div> + </div> + <div class="stack stack-2" role="region" aria-label="Stack 2 (Teal)" id="stack-2"> + <h3>Stack 2 (Teal)</h3> + <div class="stack-items" id="stack-2-items" aria-live="polite"></div> + </div> + <div class="stack stack-3" role="region" aria-label="Stack 3 (Blue)" id="stack-3"> + <h3>Stack 3 (Blue)</h3> + <div class="stack-items" id="stack-3-items" aria-live="polite"></div> + </div> + <div class="stack stack-4" role="region" aria-label="Stack 4 (Yellow)" id="stack-4"> + <h3>Stack 4 (Yellow)</h3> + <div class="stack-items" id="stack-4-items" aria-live="polite"></div> + </div> + </section> + + <section class="input-section" aria-label="Command input"> + <h2 class="sr-only">Command Input</h2> + <div class="input-container"> + <label for="forth-input" class="sr-only">Forth command input</label> + <input type="text" id="forth-input" placeholder="Enter Forth commands here..." aria-describedby="input-help" /> + <button type="button" onclick="executeForth()" aria-label="Execute command">Run</button> + <button type="button" onclick="clearAll()" aria-label="Clear all stacks">Clear</button> + </div> + <div id="input-help" class="sr-only">Press Enter to execute commands</div> + </section> + + <section class="output-section" aria-label="Command output"> + <h2 class="sr-only">Output</h2> + <div class="output" id="output" role="log" aria-live="polite" aria-label="Forth interpreter output"></div> + </section> + </main> + + <script src="forth.js"></script> + <script> + let forthState = ForthInterpreter.createInitialState(); + + + + // Update visual display + const updateDisplay = () => { + // Update stacks + forthState.stacks.forEach((stack, index) => { + const container = document.getElementById(`stack-${index + 1}-items`); + container.innerHTML = ''; + stack.forEach(item => { + const div = document.createElement('div'); + div.className = 'stack-item'; + div.textContent = item.toString(); + container.appendChild(div); + }); + }); + + // Update focus indicator + document.querySelectorAll('.stack').forEach((stack, index) => { + if (index === forthState.focusedStack) { + stack.classList.add('focused'); + } else { + stack.classList.remove('focused'); + } + }); + + // Update focus text + const focusNames = ['Red (1)', 'Teal (2)', 'Blue (3)', 'Yellow (4)']; + document.getElementById('current-focus').textContent = focusNames[forthState.focusedStack]; + + // Update output + const outputElement = document.getElementById('output'); + outputElement.innerHTML = forthState.output + .slice(-100) // Show last 100 messages for better help display + .map(msg => { + const className = msg.startsWith('Error:') ? 'error' : 'success'; + return `<div class="${className}">${msg}</div>`; + }) + .join(''); + outputElement.scrollTop = outputElement.scrollHeight; + }; + + // Execute Forth commands + const executeForth = () => { + const input = document.getElementById('forth-input'); + const command = input.value.trim(); + + if (command) { + forthState = ForthInterpreter.parseAndExecute(forthState, command); + updateDisplay(); + input.value = ''; + } + }; + + // Clear all stacks + const clearAll = () => { + forthState = ForthInterpreter.createInitialState(); + updateDisplay(); + }; + + // Handle Enter key in input + document.getElementById('forth-input').addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + executeForth(); + } + }); + + // Initialize display + updateDisplay(); + </script> +</body> +</html> \ No newline at end of file diff --git a/forth/foreforthfourth/test-advanced.js b/forth/foreforthfourth/test-advanced.js new file mode 100644 index 0000000..330fd81 --- /dev/null +++ b/forth/foreforthfourth/test-advanced.js @@ -0,0 +1,94 @@ +// Advanced test file for string operations and control flow +// Run with: node test-advanced.js + +const ForthInterpreter = require('./forth.js'); + +console.log('🧪 Testing Advanced 4-Stack Forth Features\n'); + +// Test 1: String literals +console.log('Test 1: String literals'); +let state = ForthInterpreter.createInitialState(); +state = ForthInterpreter.parseAndExecute(state, '." Hello World"'); +console.log('Stack 1 after string literal:', state.stacks[0]); +console.log('Expected: ["Hello World"]\n'); + +// Test 2: String operations +console.log('Test 2: String operations'); +state = ForthInterpreter.parseAndExecute(state, 'dup strlen'); +console.log('Stack 1 after strlen:', state.stacks[0]); +console.log('Expected: ["Hello World", 11]\n'); + +// Test 3: String concatenation +console.log('Test 3: String concatenation'); +state = ForthInterpreter.parseAndExecute(state, '." from Forth" strcat'); +console.log('Stack 1 after strcat:', state.stacks[0]); +console.log('Expected: ["Hello World from Forth"]\n'); + +// Test 4: Basic control flow - IF THEN +console.log('Test 4: Basic control flow - IF THEN'); +state = ForthInterpreter.parseAndExecute(state, '5 3 > if ." Greater" then'); +console.log('Output:', state.output[state.output.length - 1]); +console.log('Expected: "Greater"\n'); + +// Test 5: Control flow with false condition +console.log('Test 5: Control flow with false condition'); +state = ForthInterpreter.parseAndExecute(state, '3 5 > if ." Greater" then'); +console.log('Output:', state.output[state.output.length - 1]); +console.log('Expected: No output (condition was false)\n'); + +// Test 6: IF ELSE THEN +console.log('Test 6: IF ELSE THEN'); +state = ForthInterpreter.parseAndExecute(state, '5 3 > if ." Greater" else ." Less or Equal" then'); +console.log('Output:', state.output[state.output.length - 1]); +console.log('Expected: "Greater"\n'); + +// Test 7: IF ELSE THEN with false condition +console.log('Test 7: IF ELSE THEN with false condition'); +state = ForthInterpreter.parseAndExecute(state, '3 5 > if ." Greater" else ." Less or Equal" then'); +console.log('Output:', state.output[state.output.length - 1]); +console.log('Expected: "Less or Equal"\n'); + +// Test 8: BEGIN UNTIL loop +console.log('Test 8: BEGIN UNTIL loop'); +state = ForthInterpreter.parseAndExecute(state, '5 begin dup ." Loop " 1 - dup 0 = until drop'); +console.log('Output:', state.output.slice(-5)); +console.log('Expected: 5 loop iterations\n'); + +// Test 9: Complex control flow +console.log('Test 9: Complex control flow'); +state = ForthInterpreter.parseAndExecute(state, '10 begin dup 0 > if dup ." Count: " . 1 - else drop 0 then dup 0 = until'); +console.log('Output:', state.output.slice(-10)); +console.log('Expected: Countdown from 10 to 1\n'); + +// Test 10: String manipulation with control flow +console.log('Test 10: String manipulation with control flow'); +state = ForthInterpreter.parseAndExecute(state, '." Test" dup strlen 5 > if ." Long string" else ." Short string" then'); +console.log('Output:', state.output.slice(-3)); +console.log('Expected: "Test", "Short string"\n'); + +// Test 11: Nested control flow +console.log('Test 11: Nested control flow'); +state = ForthInterpreter.parseAndExecute(state, '7 dup 5 > if dup 10 > if ." Very large" else ." Large" then else dup 3 > if ." Medium" else ." Small" then then'); +console.log('Output:', state.output[state.output.length - 1]); +console.log('Expected: "Large"\n'); + +// Test 12: String operations with numbers +console.log('Test 12: String operations with numbers'); +state = ForthInterpreter.parseAndExecute(state, '." Hello" 32 char+'); +console.log('Stack 1 after char+:', state.stacks[0]); +console.log('Expected: ["Hello "]\n'); + +console.log('✅ All advanced tests completed!'); +console.log('\nFinal state:'); +console.log('Stack 1:', state.stacks[0]); +console.log('Stack 2:', state.stacks[1]); +console.log('Stack 3:', state.stacks[2]); +console.log('Stack 4:', state.stacks[3]); +console.log('Dictionary size:', state.dictionary.size); +console.log('Output messages:', state.output.length); + +// Show some example outputs +console.log('\nSample outputs:'); +state.output.slice(-5).forEach((msg, i) => { + console.log(`${i + 1}. ${msg}`); +}); diff --git a/forth/foreforthfourth/test-cross-stack-complete.js b/forth/foreforthfourth/test-cross-stack-complete.js new file mode 100644 index 0000000..22f7a16 --- /dev/null +++ b/forth/foreforthfourth/test-cross-stack-complete.js @@ -0,0 +1,373 @@ +const ForthInterpreter = require('./forth.js'); + +console.log('🧪 Comprehensive Cross-Stack Operations Test Suite\n'); + +let state = ForthInterpreter.createInitialState(); +let testCount = 0; +let passCount = 0; + +// Test helper function +const test = (name, testFn) => { + testCount++; + try { + testFn(); + console.log(`✅ ${name}`); + passCount++; + } catch (error) { + console.log(`❌ ${name}: ${error.message}`); + } +}; + +// Test 1: Basic Cross-Stack Operations +test('dup.stacks - Basic Functionality', () => { + state = ForthInterpreter.createInitialState(); + + // Set up source stack + state = ForthInterpreter.parseAndExecute(state, 'focus.red'); + state = ForthInterpreter.parseAndExecute(state, '42'); + + // Execute dup.stacks + state = ForthInterpreter.parseAndExecute(state, 'dup.stacks'); + if (!state.crossStackInProgress) throw new Error('Should set cross-stack mode'); + if (state.crossStackOperation !== 'dup') throw new Error('Should set operation to dup'); + + // Specify target stack + state = ForthInterpreter.parseAndExecute(state, '2'); // Target Teal stack + if (state.crossStackInProgress) throw new Error('Should clear cross-stack mode'); + + // Verify results + if (state.stacks[0].length !== 1) throw new Error('Source stack should still have 1 item'); + if (state.stacks[1].length !== 1) throw new Error('Target stack should have 1 item'); + if (state.stacks[0][state.stacks[0].length - 1] !== 42) throw new Error('Source stack should still have 42'); + if (state.stacks[1][state.stacks[1].length - 1] !== 42) throw new Error('Target stack should have 42'); +}); + +test('over.stacks - Copy Second Item', () => { + state = ForthInterpreter.createInitialState(); + + // Set up source stack with multiple items + state = ForthInterpreter.parseAndExecute(state, 'focus.blue'); + state = ForthInterpreter.parseAndExecute(state, '10'); + state = ForthInterpreter.parseAndExecute(state, '20'); + state = ForthInterpreter.parseAndExecute(state, '30'); + + // Execute over.stacks + state = ForthInterpreter.parseAndExecute(state, 'over.stacks'); + if (!state.crossStackInProgress) throw new Error('Should set cross-stack mode'); + + // Specify target stack + state = ForthInterpreter.parseAndExecute(state, '1'); // Target Red stack + if (state.crossStackInProgress) throw new Error('Should clear cross-stack mode'); + + // Verify results + if (state.stacks[2].length !== 3) throw new Error('Source stack should still have 3 items'); + if (state.stacks[0].length !== 1) throw new Error('Target stack should have 1 item'); + if (state.stacks[0][state.stacks[0].length - 1] !== 20) throw new Error('Target stack should have second item (20)'); +}); + +test('swap.stacks - Swap Top Items', () => { + state = ForthInterpreter.createInitialState(); + + // Set up source stack + state = ForthInterpreter.parseAndExecute(state, 'focus.yellow'); + state = ForthInterpreter.parseAndExecute(state, '100'); + state = ForthInterpreter.parseAndExecute(state, '200'); + + // Execute swap.stacks + state = ForthInterpreter.parseAndExecute(state, 'swap.stacks'); + if (!state.crossStackInProgress) throw new Error('Should set cross-stack mode'); + + // Specify target stack + state = ForthInterpreter.parseAndExecute(state, '3'); // Target Blue stack + if (state.crossStackInProgress) throw new Error('Should clear cross-stack mode'); + + // Verify results + if (state.stacks[3].length !== 2) throw new Error('Source stack should still have 2 items'); + if (state.stacks[2].length !== 2) throw new Error('Target stack should have 2 items'); + if (state.stacks[3][state.stacks[3].length - 1] !== 200) throw new Error('Source stack top should be 200'); + if (state.stacks[3][state.stacks[3].length - 2] !== 100) throw new Error('Source stack second should be 100'); + if (state.stacks[2][state.stacks[2].length - 1] !== 100) throw new Error('Target stack top should be 100'); + if (state.stacks[2][state.stacks[2].length - 2] !== 200) throw new Error('Target stack second should be 200'); +}); + +test('nip.stacks - Move Second Item', () => { + state = ForthInterpreter.createInitialState(); + + // Set up source stack + state = ForthInterpreter.parseAndExecute(state, 'focus.teal'); + state = ForthInterpreter.parseAndExecute(state, '50'); + state = ForthInterpreter.parseAndExecute(state, '60'); + + // Execute nip.stacks + state = ForthInterpreter.parseAndExecute(state, 'nip.stacks'); + if (!state.crossStackInProgress) throw new Error('Should set cross-stack mode'); + + // Specify target stack + state = ForthInterpreter.parseAndExecute(state, '4'); // Target Yellow stack + if (state.crossStackInProgress) throw new Error('Should clear cross-stack mode'); + + // Verify results + if (state.stacks[1].length !== 1) throw new Error('Source stack should have 1 item after nip'); + if (state.stacks[3].length !== 1) throw new Error('Target stack should have 1 item'); + if (state.stacks[1][state.stacks[1].length - 1] !== 60) throw new Error('Source stack should keep top item (60)'); + if (state.stacks[3][state.stacks[3].length - 1] !== 50) throw new Error('Target stack should have second item (50)'); +}); + +test('tuck.stacks - Tuck Operation', () => { + state = ForthInterpreter.createInitialState(); + + // Set up source stack + state = ForthInterpreter.parseAndExecute(state, 'focus.red'); + state = ForthInterpreter.parseAndExecute(state, '7'); + state = ForthInterpreter.parseAndExecute(state, '8'); + + // Execute tuck.stacks + state = ForthInterpreter.parseAndExecute(state, 'tuck.stacks'); + if (!state.crossStackInProgress) throw new Error('Should set cross-stack mode'); + + // Specify target stack + state = ForthInterpreter.parseAndExecute(state, '2'); // Target Teal stack + if (state.crossStackInProgress) throw new Error('Should clear cross-stack mode'); + + // Verify results + if (state.stacks[0].length !== 1) throw new Error('Source stack should have 1 item after tuck'); + if (state.stacks[1].length !== 3) throw new Error('Target stack should have 3 items'); + if (state.stacks[0][state.stacks[0].length - 1] !== 7) throw new Error('Source stack should keep top item (7)'); + if (state.stacks[1][state.stacks[1].length - 1] !== 8) throw new Error('Target stack should have 8 at top'); + if (state.stacks[1][state.stacks[1].length - 2] !== 7) throw new Error('Target stack should have 7 in middle'); + if (state.stacks[1][state.stacks[1].length - 3] !== 8) throw new Error('Target stack should have 8 at bottom'); +}); + +test('rot.stacks - Rotate Top 3 Items', () => { + state = ForthInterpreter.createInitialState(); + + // Set up source stack + state = ForthInterpreter.parseAndExecute(state, 'focus.blue'); + state = ForthInterpreter.parseAndExecute(state, '1'); + state = ForthInterpreter.parseAndExecute(state, '2'); + state = ForthInterpreter.parseAndExecute(state, '3'); + + // Execute rot.stacks + state = ForthInterpreter.parseAndExecute(state, 'rot.stacks'); + if (!state.crossStackInProgress) throw new Error('Should set cross-stack mode'); + + // Specify target stack + state = ForthInterpreter.parseAndExecute(state, '1'); // Target Red stack + if (state.crossStackInProgress) throw new Error('Should clear cross-stack mode'); + + // Verify results + if (state.stacks[2].length !== 3) throw new Error('Source stack should still have 3 items'); + if (state.stacks[0].length !== 3) throw new Error('Target stack should have 3 items'); + // Source stack should be rotated: [1, 3, 2] (top=2, second=3, third=1) + if (state.stacks[2][state.stacks[2].length - 1] !== 2) throw new Error('Source stack top should be 2'); + if (state.stacks[2][state.stacks[2].length - 2] !== 3) throw new Error('Source stack second should be 3'); + if (state.stacks[2][state.stacks[2].length - 3] !== 1) throw new Error('Source stack third should be 1'); + // Target stack should have rotated items: [1, 3, 2] (top=2, second=3, third=1) + if (state.stacks[0][state.stacks[0].length - 1] !== 2) throw new Error('Target stack top should be 2'); + if (state.stacks[0][state.stacks[0].length - 2] !== 3) throw new Error('Target stack second should be 3'); + if (state.stacks[0][state.stacks[0].length - 3] !== 1) throw new Error('Target stack third should be 1'); +}); + +test('2dup.stacks - Duplicate Top 2 Items', () => { + state = ForthInterpreter.createInitialState(); + + // Set up source stack + state = ForthInterpreter.parseAndExecute(state, 'focus.yellow'); + state = ForthInterpreter.parseAndExecute(state, '25'); + state = ForthInterpreter.parseAndExecute(state, '35'); + + // Execute 2dup.stacks + state = ForthInterpreter.parseAndExecute(state, '2dup.stacks'); + if (!state.crossStackInProgress) throw new Error('Should set cross-stack mode'); + + // Specify target stack + state = ForthInterpreter.parseAndExecute(state, '3'); // Target Blue stack + if (state.crossStackInProgress) throw new Error('Should clear cross-stack mode'); + + // Verify results + if (state.stacks[3].length !== 2) throw new Error('Source stack should still have 2 items'); + if (state.stacks[2].length !== 2) throw new Error('Target stack should have 2 items'); + if (state.stacks[3][state.stacks[3].length - 1] !== 35) throw new Error('Source stack top should be 35'); + if (state.stacks[3][state.stacks[3].length - 2] !== 25) throw new Error('Source stack second should be 25'); + if (state.stacks[2][state.stacks[2].length - 1] !== 35) throw new Error('Target stack top should be 35'); + if (state.stacks[2][state.stacks[2].length - 2] !== 25) throw new Error('Target stack second should be 25'); +}); + +test('2over.stacks - Copy Second Pair', () => { + state = ForthInterpreter.createInitialState(); + + // Set up source stack with 4 items + state = ForthInterpreter.parseAndExecute(state, 'focus.red'); + state = ForthInterpreter.parseAndExecute(state, '10'); + state = ForthInterpreter.parseAndExecute(state, '20'); + state = ForthInterpreter.parseAndExecute(state, '30'); + state = ForthInterpreter.parseAndExecute(state, '40'); + + // Execute 2over.stacks + state = ForthInterpreter.parseAndExecute(state, '2over.stacks'); + if (!state.crossStackInProgress) throw new Error('Should set cross-stack mode'); + + // Specify target stack + state = ForthInterpreter.parseAndExecute(state, '2'); // Target Teal stack + if (state.crossStackInProgress) throw new Error('Should clear cross-stack mode'); + + // Verify results + if (state.stacks[0].length !== 4) throw new Error('Source stack should still have 4 items'); + if (state.stacks[1].length !== 2) throw new Error('Target stack should have 2 items'); + if (state.stacks[1][state.stacks[1].length - 1] !== 20) throw new Error('Target stack top should be 20'); + if (state.stacks[1][state.stacks[1].length - 2] !== 30) throw new Error('Target stack second should be 30'); +}); + +test('2swap.stacks - Swap Top 2 Pairs', () => { + state = ForthInterpreter.createInitialState(); + + // Set up source stack with 4 items + state = ForthInterpreter.parseAndExecute(state, 'focus.teal'); + state = ForthInterpreter.parseAndExecute(state, '1'); + state = ForthInterpreter.parseAndExecute(state, '2'); + state = ForthInterpreter.parseAndExecute(state, '3'); + state = ForthInterpreter.parseAndExecute(state, '4'); + + // Execute 2swap.stacks + state = ForthInterpreter.parseAndExecute(state, '2swap.stacks'); + if (!state.crossStackInProgress) throw new Error('Should set cross-stack mode'); + + // Specify target stack + state = ForthInterpreter.parseAndExecute(state, '1'); // Target Red stack + if (state.crossStackInProgress) throw new Error('Should clear cross-stack mode'); + + // Verify results + if (state.stacks[1].length !== 4) throw new Error('Source stack should still have 4 items'); + if (state.stacks[0].length !== 4) throw new Error('Target stack should have 4 items'); + // Source stack should be: [1, 2, 3, 4] (top=4, second=3, third=2, fourth=1) + if (state.stacks[1][state.stacks[1].length - 1] !== 4) throw new Error('Source stack top should be 4'); + if (state.stacks[1][state.stacks[1].length - 2] !== 3) throw new Error('Source stack second should be 3'); + if (state.stacks[1][state.stacks[1].length - 3] !== 2) throw new Error('Source stack third should be 2'); + if (state.stacks[1][state.stacks[1].length - 4] !== 1) throw new Error('Source stack fourth should be 1'); + // Target stack should be: [1, 2, 3, 4] (top=4, second=3, third=2, fourth=1) + if (state.stacks[0][state.stacks[0].length - 1] !== 4) throw new Error('Target stack top should be 4'); + if (state.stacks[0][state.stacks[0].length - 2] !== 3) throw new Error('Target stack second should be 3'); + if (state.stacks[0][state.stacks[0].length - 3] !== 2) throw new Error('Target stack third should be 2'); + if (state.stacks[0][state.stacks[0].length - 4] !== 1) throw new Error('Target stack fourth should be 1'); +}); + +// Test 2: Error Handling +test('Error Handling - Stack Underflow', () => { + state = ForthInterpreter.createInitialState(); + + // Try dup.stacks on empty stack + state = ForthInterpreter.parseAndExecute(state, 'focus.red'); + state = ForthInterpreter.parseAndExecute(state, 'dup.stacks'); + + const lastOutput = state.output[state.output.length - 1]; + if (!lastOutput.includes('Error: Stack underflow')) { + throw new Error('Should show stack underflow error'); + } +}); + +test('Error Handling - Invalid Target Stack', () => { + state = ForthInterpreter.createInitialState(); + + // Set up valid operation + state = ForthInterpreter.parseAndExecute(state, 'focus.red'); + state = ForthInterpreter.parseAndExecute(state, '42'); + state = ForthInterpreter.parseAndExecute(state, 'dup.stacks'); + + // Try invalid target stack + state = ForthInterpreter.parseAndExecute(state, '5'); // Invalid stack number + + const lastOutput = state.output[state.output.length - 1]; + if (!lastOutput.includes('Error: Invalid target stack')) { + throw new Error('Should show invalid target stack error'); + } + if (state.crossStackInProgress) { + throw new Error('Should clear cross-stack mode on error'); + } +}); + +// Test 3: Edge Cases +test('Edge Cases - Single Item Operations', () => { + state = ForthInterpreter.createInitialState(); + + // Test over.stacks with only 1 item + state = ForthInterpreter.parseAndExecute(state, 'focus.blue'); + state = ForthInterpreter.parseAndExecute(state, '100'); + state = ForthInterpreter.parseAndExecute(state, 'over.stacks'); + + const lastOutput = state.output[state.output.length - 1]; + if (!lastOutput.includes('Error: Stack underflow')) { + throw new Error('Should show stack underflow error for over.stacks with 1 item'); + } +}); + +test('Edge Cases - Focus Persistence', () => { + state = ForthInterpreter.createInitialState(); + + // Set focus and perform operation + state = ForthInterpreter.parseAndExecute(state, 'focus.yellow'); + const originalFocus = state.focusedStack; + + state = ForthInterpreter.parseAndExecute(state, '50'); + state = ForthInterpreter.parseAndExecute(state, 'dup.stacks'); + state = ForthInterpreter.parseAndExecute(state, '1'); + + if (state.focusedStack !== originalFocus) { + throw new Error('Focus should persist through cross-stack operations'); + } +}); + +// Test 4: Complex Workflows +test('Complex Workflows - Multi-Operation Chain', () => { + state = ForthInterpreter.createInitialState(); + + // Set up multiple stacks + state = ForthInterpreter.parseAndExecute(state, 'focus.red'); + state = ForthInterpreter.parseAndExecute(state, '100'); + state = ForthInterpreter.parseAndExecute(state, '200'); + + state = ForthInterpreter.parseAndExecute(state, 'focus.teal'); + state = ForthInterpreter.parseAndExecute(state, '300'); + + // Chain operations + state = ForthInterpreter.parseAndExecute(state, 'dup.stacks'); + state = ForthInterpreter.parseAndExecute(state, '1'); // Target Red + + state = ForthInterpreter.parseAndExecute(state, 'focus.red'); + state = ForthInterpreter.parseAndExecute(state, 'over.stacks'); + state = ForthInterpreter.parseAndExecute(state, '3'); // Target Blue + + // Verify final state + if (state.stacks[0].length !== 3) throw new Error('Red stack should have 3 items'); + if (state.stacks[1].length !== 1) throw new Error('Teal stack should have 1 item'); + if (state.stacks[2].length !== 1) throw new Error('Blue stack should have 1 item'); +}); + +// Test 5: State Management +test('State Management - Cross-Stack Mode Reset', () => { + state = ForthInterpreter.createInitialState(); + + // Start operation + state = ForthInterpreter.parseAndExecute(state, 'focus.red'); + state = ForthInterpreter.parseAndExecute(state, '42'); + state = ForthInterpreter.parseAndExecute(state, 'dup.stacks'); + + if (!state.crossStackInProgress) throw new Error('Should set cross-stack mode'); + + // Complete operation + state = ForthInterpreter.parseAndExecute(state, '2'); + + if (state.crossStackInProgress) throw new Error('Should clear cross-stack mode'); + if (state.crossStackOperation !== null) throw new Error('Should clear operation'); + if (state.crossStackData !== null) throw new Error('Should clear data'); +}); + +console.log(`\n📊 Test Results: ${passCount}/${testCount} tests passed`); +console.log(`🎯 Success Rate: ${((passCount / testCount) * 100).toFixed(1)}%`); + +if (passCount === testCount) { + console.log('\n🎉 All cross-stack operation tests passed!'); +} else { + console.log('\n⚠️ Some tests failed. Please review the implementation.'); +} + +console.log('\n🚀 Cross-stack operations test suite complete!'); diff --git a/forth/foreforthfourth/test-forth.js b/forth/foreforthfourth/test-forth.js new file mode 100644 index 0000000..54f9963 --- /dev/null +++ b/forth/foreforthfourth/test-forth.js @@ -0,0 +1,77 @@ +// Simple test file for the Forth interpreter +// Run with: node test-forth.js + +const ForthInterpreter = require('./forth.js'); + +console.log('🧪 Testing 4-Stack Toy Forth Interpreter\n'); + +// Test 1: Basic number pushing +console.log('Test 1: Basic number pushing'); +let state = ForthInterpreter.createInitialState(); +state = ForthInterpreter.parseAndExecute(state, '5 3 2'); +console.log('Stack 1 after "5 3 2":', state.stacks[0]); +console.log('Expected: [5, 3, 2]\n'); + +// Test 2: Basic arithmetic +console.log('Test 2: Basic arithmetic'); +state = ForthInterpreter.parseAndExecute(state, '+'); +console.log('Stack 1 after "+":', state.stacks[0]); +console.log('Expected: [5, 5] (3+2=5)\n'); + +// Test 3: Stack manipulation +console.log('Test 3: Stack manipulation'); +state = ForthInterpreter.parseAndExecute(state, 'dup over'); +console.log('Stack 1 after "dup over":', state.stacks[0]); +console.log('Expected: [5, 5, 5, 5] (dup then over)\n'); + +// Test 4: Stack inspection +console.log('Test 4: Stack inspection'); +state = ForthInterpreter.parseAndExecute(state, '.s'); +console.log('Output:', state.output[state.output.length - 1]); +console.log('Expected: <4> 5 5 5 5\n'); + +// Test 5: Comparison operators +console.log('Test 5: Comparison operators'); +state = ForthInterpreter.parseAndExecute(state, '5 3 >'); +console.log('Stack 1 after "5 3 >":', state.stacks[0]); +console.log('Expected: [5, 5, 5, 5, -1] (5 > 3 = true = -1)\n'); + +// Test 6: Word definition +console.log('Test 6: Word definition'); +state = ForthInterpreter.parseAndExecute(state, ': double dup + ;'); +console.log('Output:', state.output[state.output.length - 1]); +console.log('Expected: Word \'double\' defined\n'); + +// Test 7: Using defined word +console.log('Test 7: Using defined word'); +state = ForthInterpreter.parseAndExecute(state, 'double'); +console.log('Stack 1 after "double":', state.stacks[0]); +console.log('Expected: [5, 5, 5, 5, -1, 10] (double of 5 = 10)\n'); + +// Test 8: List all words +console.log('Test 8: List all words'); +state = ForthInterpreter.parseAndExecute(state, 'words'); +console.log('Output:', state.output.slice(-3)); +console.log('Expected: Built-in words, User defined words, Total words count\n'); + +// Test 9: Stack juggling +console.log('Test 9: Stack juggling'); +state = ForthInterpreter.parseAndExecute(state, 'push.teal'); +console.log('Stack 1 after "push.teal":', state.stacks[0]); +console.log('Stack 2 after "push.teal":', state.stacks[1]); +console.log('Expected: Stack 1: [5, 5, 5, 5, -1], Stack 2: [10]\n'); + +// Test 10: Error handling +console.log('Test 10: Error handling'); +state = ForthInterpreter.parseAndExecute(state, 'drop drop drop drop drop drop drop drop drop drop drop'); +console.log('Output:', state.output[state.output.length - 1]); +console.log('Expected: Error: Stack underflow on drop\n'); + +console.log('✅ All tests completed!'); +console.log('\nFinal state:'); +console.log('Stack 1:', state.stacks[0]); +console.log('Stack 2:', state.stacks[1]); +console.log('Stack 3:', state.stacks[2]); +console.log('Stack 4:', state.stacks[3]); +console.log('Dictionary size:', state.dictionary.size); +console.log('Output messages:', state.output.length); diff --git a/forth/foreforthfourth/test-help-full.js b/forth/foreforthfourth/test-help-full.js new file mode 100644 index 0000000..bf257ec --- /dev/null +++ b/forth/foreforthfourth/test-help-full.js @@ -0,0 +1,33 @@ +// Test to see the complete help output +const ForthInterpreter = require('./forth.js'); + +console.log('🔍 Testing Complete Help Output\n'); + +let state = ForthInterpreter.createInitialState(); + +// Run help command +console.log('Running help command...'); +state = ForthInterpreter.parseAndExecute(state, 'help'); + +console.log('\n=== COMPLETE HELP OUTPUT ==='); +state.output.forEach((line, i) => { + console.log(`${i + 1}: ${line}`); +}); + +console.log('\n=== ANALYSIS ==='); +console.log('Total output lines:', state.output.length); + +// Count built-in words in help output +const helpLines = state.output.filter(line => line.includes(' - ')); +console.log('Lines with word documentation:', helpLines.length); + +// Check if specific words are present +const expectedWords = ['dup', 'swap', 'drop', '+', '-', '*', '/', 'mod', 'if', 'then', 'begin', 'until']; +expectedWords.forEach(word => { + const found = helpLines.some(line => line.startsWith(word)); + console.log(`${word}: ${found ? '✅' : '❌'}`); +}); + +// Show first few documented words +console.log('\nFirst 10 documented words:'); +helpLines.slice(0, 10).forEach(line => console.log(line)); diff --git a/forth/foreforthfourth/test-help.js b/forth/foreforthfourth/test-help.js new file mode 100644 index 0000000..b3aa28e --- /dev/null +++ b/forth/foreforthfourth/test-help.js @@ -0,0 +1,52 @@ +// Test the new help system +const ForthInterpreter = require('./forth.js'); + +console.log('🧪 Testing Help System\n'); + +let state = ForthInterpreter.createInitialState(); + +// Test 1: Basic help command +console.log('Test 1: Basic help command'); +state = ForthInterpreter.parseAndExecute(state, 'help'); +console.log('Help output (first 10 lines):'); +state.output.slice(-10).forEach((line, i) => { + console.log(`${i + 1}. ${line}`); +}); + +// Test 2: Document specific word +console.log('\nTest 2: Document specific word'); +state = ForthInterpreter.parseAndExecute(state, 's" dup"'); +state = ForthInterpreter.parseAndExecute(state, 'doc'); +console.log('Doc output:'); +state.output.slice(-5).forEach((line, i) => { + console.log(`${i + 1}. ${line}`); +}); + +// Test 3: Document another word +console.log('\nTest 3: Document another word'); +state = ForthInterpreter.parseAndExecute(state, 's" +"'); +state = ForthInterpreter.parseAndExecute(state, 'doc'); +console.log('Doc output:'); +state.output.slice(-5).forEach((line, i) => { + console.log(`${i + 1}. ${line}`); +}); + +// Test 4: Document non-existent word +console.log('\nTest 4: Document non-existent word'); +state = ForthInterpreter.parseAndExecute(state, 's" nonexistent"'); +state = ForthInterpreter.parseAndExecute(state, 'doc'); +console.log('Doc output:'); +state.output.slice(-3).forEach((line, i) => { + console.log(`${i + 1}. ${line}`); +}); + +// Test 5: Words command +console.log('\nTest 5: Words command'); +state = ForthInterpreter.parseAndExecute(state, 'words'); +console.log('Words output:'); +state.output.slice(-3).forEach((line, i) => { + console.log(`${i + 1}. ${line}`); +}); + +console.log('\n✅ Help system tests completed!'); +console.log('Total output lines:', state.output.length); diff --git a/forth/guesser.fth b/forth/guesser.fth new file mode 100644 index 0000000..01d84a0 --- /dev/null +++ b/forth/guesser.fth @@ -0,0 +1,100 @@ +\ --- Number Guessing Game for pForth --- + +\ First, we need some helper words for reliable numeric input. +\ These are based on the pForth tutorial. +( -- $addr ) +: INPUT$ ( A word to get a line of text from the user ) + PAD 1+ ( addr --- , leave room on the scratchpad for a byte count ) + 80 ACCEPT ( addr maxbytes -- numbytes , get up to 80 chars ) + PAD C! ( numbytes --- , store the count in the first byte ) + PAD ( -- $addr , leave the address of the counted string ) +; + +( -- n true | false ) +: INPUT# ( Read a line and convert it to a number ) + INPUT$ ( -- $addr ) + NUMBER? ( $addr -- d_num true | false , convert string to double ) + IF + SWAP DROP TRUE ( d_num true -- n true , drop high part of double ) + ELSE + FALSE ( -- false ) + THEN +; + +\ --- Game Logic --- + +VARIABLE SECRET# \ A place to store the secret number. + +( -- ) +: INIT-SECRET# ( Generate and store a random number ) + 501 CHOOSE ( -- rand , CHOOSE gives a number from 0 to n-1 ) + SECRET# ! ( rand -- , store it in our variable ) +; + +( -- n ) +: GET-GUESS ( Loop until the user enters a valid number ) + BEGIN + CR ." Your guess (0-500)? " + INPUT# ( -- n true | false ) + UNTIL ( loops until flag is true ) +; + +( guess -- correct? ) +: CHECK-GUESS ( Compares guess to the secret number, gives a hint ) + SECRET# @ ( guess -- guess secret# ) + 2DUP = ( guess secret# -- guess secret# flag ) + IF ( guess is equal to secret# ) + ." You got it!" CR + 2DROP TRUE \ --> Make sure this 2DROP is here. + ELSE ( guess is not equal ) + 2DUP < ( guess secret# -- guess secret# flag ) + IF + ." Too low!" CR + 2DROP FALSE \ --> And this one. + ELSE + ." Too high!" CR + 2DROP FALSE \ --> And this one. + THEN + THEN +; + +( -- ) +: SEED-BY-WAITING ( Uses user's reaction time to seed the PRNG ) + CR ." Press any key to begin..." + BEGIN + 501 CHOOSE DROP \ "Burn" a random number from the sequence + ?TERMINAL \ Check if a key has been pressed + UNTIL + CR +; + +( -- ) +: GUESSING-GAME ( The main word to run the game ) + SEED-BY-WAITING \ Call our new interactive seeder + INIT-SECRET# + CR ." I'm thinking of a number between 0 and 500." + CR ." You have 5 chances to guess it." + + FALSE ( -- user-won? , place a 'false' flag on the stack ) + + 5 0 DO ( loop 5 times, from 0 to 4 ) + CR 5 I - . ." guesses left." + GET-GUESS + CHECK-GUESS ( guess -- correct? ) + IF ( a correct guess was made ) + DROP TRUE ( replace the 'user-won?' flag with 'true' ) + LEAVE ( and exit the loop immediately ) + THEN + LOOP + + ( The 'user-won?' flag is now on top of the stack ) + IF + CR ." Congratulations!" CR + ELSE + CR ." Sorry, you ran out of guesses." + CR ." The number was " SECRET# @ . CR + THEN +; + +GUESSING-GAME \ This line executes the game word we just defined. +BYE \ This tells pForth to exit when the game is over. diff --git a/forth/pf_ref.md b/forth/pf_ref.md new file mode 100644 index 0000000..92ebbb6 --- /dev/null +++ b/forth/pf_ref.md @@ -0,0 +1,1213 @@ +::::::::::::: {#container} +:::::: {#header} +::: {#leftheader} +[{width="200" height="100" +border="0"}](/) +::: + +::: {#rightheader} +::: + +::: {#midheader} +# SoftSynth + +## \... music and computers \... +::: + +\ +:::::: + +:::: {#leftside} +::: {#leftside_inner} +- [Home](/index.php) +- [Products](/products.php) +- [JSyn](/jsyn/index.php) +- [Syntona](/syntona/index.php) +- [pForth](/pforth/index.php) +- [Music](/music/index.php) +- [Info](/info/index.php) +- [News](/news/index.php) +- [Links](/links/index.php) +- [Contact Us](/contacts.php) +- [About Us](/aboutus.php) +::: +:::: + +:::: {#rightside} +::: {#rightside_inner} +### Projects + + --------------------------------------------------------------------------------------------------- + [JSyn](/jsyn/) - modular synthesis API for Java. + [JMSL](https://www.algomusic.com/jmsl/){target="_blank"} - Java Music Specification Language + [PortAudio](https://www.portaudio.com/){target="_blank"} - cross platform audio I/O API for \'C\' + --------------------------------------------------------------------------------------------------- +::: +:::: + +::: {#content} +[pForth](/pforth/index.php) + : [GitHub](https://github.com/philburk/pforth/) + \| [Tutorial](/pforth/pf_tut.php) \| [Reference]{.current_link} + \| [Links](/forthlinks.php) + +------------------------------------------------------------------------ + +# pForth Reference Manual + +------------------------------------------------------------------------ + +### pForth - a Portable ANSI style Forth written in ANSI \'C\'. + +### **Last updated: July 21, 2016 V23** + +by Phil Burk with Larry Polansky, David Rosenboom. Special thanks to +contributors Darren Gibbs, Herb Maeder, Gary Arakaki, Mike Haas. + +Back to [pForth Home Page](../pforth) + +## LEGAL NOTICE + +The pForth software code is dedicated to the public domain, and any +third party may reproduce, distribute and modify the pForth software +code or any derivative works thereof without any compensation or +license. The pForth software code is provided on an \"as is\" basis +without any warranty of any kind, including, without limitation, the +implied warranties of merchantability and fitness for a particular +purpose and their equivalents under the laws of any jurisdiction. + +------------------------------------------------------------------------ + +## Table of Contents + +- [What is pForth?](#what-is) +- [Compiling pForth for your System](#Compiling-pForth-System) + - [Description of Source Files](#Description-Files) +- [Running pForth](#Running-pForth) +- [ANSI Compliance](#ANSI-Compliance) +- [pForth Special Features](#pForth-Features) + - [Compiling from a File - INCLUDE](#Compiling-File) + - [Saving Precompiled Dictionaries](#Saving-Dictionaries) + - [Creating Turnkey Applications](#Turnkey-Apps) + - [Recompiling Code - ANEW INCLUDE?](#Recompiling-Code) + - [Customising Forget with \[FORGET\]](#Customising-FORGET) + - [Smart Conditionals](#Smart-Conditionals) + - [Development Tools](#Development-Tools) + - [WORDS.LIKE](#WORDS.LIKE) + - [FILE?](#FILEQ) + - [SEE](#SEE) + - [Single Step Trace and Debug](#single-step-trace) + - [Conditional Compilation - \[IF\] \[ELSE\] + \[THEN\]](#Conditional-Compilation) + - [Miscellaneous Handy Words](#Miscellaneous-Words) + - [Local Variables { foo \-- }](#Local-Variables) + - [\'C\' like Structures. :STRUCT](#C-Structures) + - [Vectorred execution - DEFER](#Vectorred-Execution) + - [Floating Point](#Floating-Point) +- [pForth Design](#pForth-Design) + - [\'C\' kernel](#C-kernel) + - [Dictionary Structures](#Dictionary-Structures) +- [Compiling pForth](#Compiling-pForth) + - [Compiler Options](#Compiler-Options) + - [Building pForth on Supported Hosts](#Building-pForth-Hosts) + - [Compiling for Embedded Systems](#Compiling-Embedded) + - [Linking with Custom \'C\' Functions](#Link-Custom-C) + - [Testing your Compiled pForth](#Testing-pForth) + +------------------------------------------------------------------------ + +## []{#what-is}What is pForth? + +PForth is an ANSI style Forth designed to be portable across many +platforms. The \'P\' in pForth stands for \"Portable\". PForth is based +on a Forth kernel written in ANSI standard \'C\'. + +### What is Forth? + +Forth is a stack based language invented by astronomer Charles Moore for +controlling telescopes. Forth is an interactive language. You can enter +commands at the keyboard and have them be immediately executed, similar +to BASIC or LISP. Forth has a dictionary of words that can be executed +or used to construct new words that are then added to the dictionary. +Forth words operate on a data stack that contains numbers and addresses. + +To learn more about Forth, see the [Forth Tutorial](pf_tut.php). + +### The Origins of pForth + +PForth began as a JSR threaded 68000 Forth called HForth that was used +to support [HMSL](/hmsl/), the Hierarchical Music Specification +Language. HMSL was a music experimentation language developed by Phil +Burk, Larry Polansky and David Rosenboom while working at the Mills +College Center for Contemporary Music. Phil moved from Mills to the 3DO +Company where he ported the Forth kernel to \'C\'. It was used +extensively at 3DO as a tool for verifying ASIC design and for bringing +up new hardware platforms. At 3DO, the Forth had to run on many systems +including SUN, SGI, Macintosh, PC, Amiga, the 3DO ARM based Opera +system, and the 3DO PowerPC based M2 system. + +### pForth Design Goals + +PForth has been designed with portability as the primary design goal. As +a result, pForth avoids any fancy UNIX calls. pForth also avoids using +any clever and original ways of constructing the Forth dictionary. It +just compiles its kernel from ANSI compatible \'C\' code then loads ANS +compatible Forth code to build the dictionary. Very boring but very +likely to work on almost any platform. + +The dictionary files that can be saved from pForth are almost host +independent. They can be compiled on one processor, and then run on +another processor. as long as the endian-ness is the same. In other +words, dictionaries built on a PC will only work on a PC. Dictionaries +built on almost any other computer will work on almost any other +computer. + +PForth can be used to bring up minimal hardware systems that have very +few system services implemented. It is possible to compile pForth for +systems that only support routines to send and receive a single +character. If malloc() and free() are not available, equivalent +functions are available in standard \'C\' code. If file I/O is not +available, the dictionary can be saved as a static data array in \'C\' +source format on a host system. The dictionary in \'C\' source form is +then compiled with a custom pForth kernel to avoid having to read the +dictionary from disk. + +------------------------------------------------------------------------ + +## []{#Compiling-pForth-System}Compiling pForth for your System + +Up-to-date instructions on compiling, possibly with comments from the +community, may be found at: + +> [https://github.com/philburk/pforth/wiki/Compiling-on-Unix](https://github.com/philburk/pforth/wiki/Compiling-on-Unix){target="_blank"} + +The process of building pForth involves several steps. This process is +typically handled automatically by the Makefile or IDE Project. + +1. Compile the \'C\' based pForth kernel called \"pforth\" or + \"pforth.exe\". +2. Execute \"pforth\" with the -i option to build the dictionary from + scratch. Compile the \"system.fth\" file which will add all the top + level Forth words. This can be done in one command by entering + \"pforth -i system.fth\". +3. Save the compiled dictionary as \"pforth.dic\". +4. The next time you run pforth, the precompiled pforth.dic file will + be loaded automatically. + +### Unix and Max OS X + +A Makefile has been provided that should work on most Unix based +platforms. + +1. cd to \"platforms/unix\" folder. +2. Enter: make all +3. Enter: ./pforth + +Note that the platforms folder used to be called build. + +### []{#Description-Files}Description of Source Files + +#### Forth Source in /fth/ + + ansilocs.fth = support for ANSI (LOCAL) word + c_struct.fth = 'C' like data structures + case.fth = CASE OF ENDOF ENDCASE + catch.fth = CATCH and THROW + condcomp.fth = [IF] [ELSE] [THEN] conditional compiler + filefind.fth = FILE? + floats.fth = floating point support + forget.fth = FORGET [FORGET] IF.FORGOTTEN + loadp4th.fth = loads basic dictionary + locals.fth = { } style locals using (LOCAL) + math.fth = misc math words + member.fth = additional 'C' like data structure support + misc1.fth = miscellaneous words + misc2.fth = miscellaneous words + numberio.fth = formatted numeric input/output + private.fth = hide low level words + quit.fth = QUIT EVALUATE INTERPRET in high level + smart_if.fth = allows conditionals outside colon definition + see.fth = Forth "disassembler". Eg. SEE SPACES + strings.fth = string support + system.fth = bootstraps pForth dictionary + trace.fth = single step trace for debugging + +#### \'C\' Source in /csrc/ + + pfcompil.c = pForth compiler support + pfcustom.c = example of 'C' functions callable from pForth + pfinnrfp.h = float extensions to interpreter + pforth.h = include this in app that embeds pForth + pf_cglue.c = glue for pForth calling 'C' + pf_clib.c = replacement routines for 'C' stdlib + pf_core.c = primary words called from 'C' app that embeds pForth + pf_float.h = defines PF_FLOAT, and the floating point math functions such as fp_sin + pf_inner.c = inner interpreter + pf_guts.h = primary include file, define structures + pf_io.c = input/output + pf_main.c = basic application for standalone pForth + pf_mem.c = optional malloc() implementation + pf_save.c = save and load dictionaries + pf_text.c = string tools, error message text + pf_words.c = miscellaneous pForth words implemented + +------------------------------------------------------------------------ + +## []{#Running-pForth}Running pForth + +PForth can be run from a shell or by double clicking on its icon, +depending on the system you are using. The execution options for pForth +are described assuming that you are running it from a shell. + +Usage: + +- pforth [-i] [-dDictionaryFilename] [SourceFilename] + +-i + +Initialize pForth by building dictionary from scratch. Used when +building pForth or when debugging pForth on new systems. + + + +-dDictionaryFilename + +Specify a custom dictionary to be loaded in place of the default +\"pforth.dic\". For example: + +- - pforth -dgame.dic + +SourceFilename + +A Forth source file can be automatically compiled by passing its name to +pForth. This is useful when using Forth as an assembler or for automated +hardware testing. Remember that the source file can compile code and +execute it all in the same file. + +#### Quick Verification of pForth + +To verify that PForth is working, enter: + +- 3 4 + . + +It should print \"7 ok\". Now enter: + +- WORDS + +You should see a long list of all the words in the pForth dictionary. +Don\'t worry. You won\'t need to learn all of these. More tests are +described in the README.txt file. + +If you want to learn how to program in Forth, try our +[tutorial](pf_tut.php). + +------------------------------------------------------------------------ + +## []{#ANSI-Compliance}ANSI Compliance + +This Forth is intended to be ANS compatible. I will not claim that it is +compatible until more people bang on it. If you find areas where it +deviates from the standard, please let me know. + +Word sets supported include: + +- FLOAT +- LOCAL with support for { lv1 lv2 \| lv3 \-- } style locals +- EXCEPTION but standard throw codes not implemented +- FILE ACCESS +- MEMORY ALLOCATION + +Here are the areas that I know are not compatible: + +The ENVIRONMENT queries are not implemented. + +Word sets NOT supported include: + +- BLOCK - a matter of religion +- SEARCH ORDER +- PROGRAMMING TOOLS - only has .S ? DUMP WORDS BYE +- STRING - only has CMOVE CMOVE\> COMPARE +- DOUBLE NUMBER - but cell is 32 bits + +------------------------------------------------------------------------ + +## []{#pForth-Features}pForth Special Features + +These features are not part of the ANS standard for Forth. They have +been added to assist developers. + +### []{#Compiling-File}Compiling from a File + +Use INCLUDE to compile source code from a file: + +- INCLUDE filename + +You can nest calls to INCLUDE. INCLUDE simply redirects Forth to takes +its input from the file instead of the keyboard so you can place any +legal Forth code in the source code file. + +### []{#Saving-Dictionaries}Saving Precompiled Dictionaries + +Use SAVE-FORTH save your precompiled code to a file. To save the current +dictionary to a file called \"custom.dic\", enter: + +- c" custom.dic" SAVE-FORTH + +You can then leave pForth and use your custom dictionary by entering: + +- pforth -dcustom.dic + +On icon based systems, you may wish to name your custom dictionary +\"pforth.dic\" so that it will be loaded automatically. + +Be careful that you do not leave absolute addresses stored in the +dictionary because they will not work when you reload pForth at a +different address. Use A! to store an address in a variable in a +relocatable form and A@ to get it back if you need to. + +- VARIABLE DATA-PTR + CREATE DATA 100 ALLOT + DATA DATA-PTR ! \ storing absolute address! BAD + DATA DATA-PTR A! \ storing relocatable address! GOOD + DATA-PTR A@ \ fetch relocatable address + +### []{#Turnkey-Apps}Creating Turnkey Applications + +Use TURNKEY to save a dictionary with a word that will run +automatically. The headers (names) will be discarded to save space in +the dictionary. Suppose you have defined a word called MYAPP to prints +the ASCII code when you press a key on the keyboard. + +- : MYAPP ( -- , print key codes ) + BEGIN ." #" key dup ascii q = not + WHILE . cr REPEAT ; + +Save a dictionary named \"turnkey.dic\" that will run MYAPP. Other names +are OK. + +- c" turnkey.dic" ' MYAPP TURNKEY + +Run the app. Press some letters to see the code. Then press \'q\' to +exit. + +- ./pforth -dturnkey.dic + +### []{#Recompiling-Code}Recompiling Code - ANEW INCLUDE? + +When you are testing a file full of code, you will probably recompile +many times. You will probably want to FORGET the old code before loading +the new code. You could put a line at the beginning of your file like +this: + +- FORGET XXXX-MINE : XXXX-MINE ; + +This would automatically FORGET for you every time you load. +Unfortunately, you must define XXXX-MINE before you can ever load this +file. We have a word that will automatically define a word for you the +first time, then FORGET and redefine it each time after that. It is +called ANEW and can be found at the beginning of most Forth source +files. We use a prefix of TASK- followed by the filename just to be +consistent. This TASK-name word is handy when working with INCLUDE? as +well. Here is an example: + +- \ Start of file + INCLUDE? TASK-MYTHING.FTH MYTHING.FTH + ANEW TASK-THISFILE.FTH + \ the rest of the file follows... + +Notice that the INCLUDE? comes before the call to ANEW so that we don\'t +FORGET MYTHING.FTH every time we recompile. + +FORGET allows you to get rid of code that you have already compiled. +This is an unusual feature in a programming language. It is very +convenient in Forth but can cause problems. Most problems with FORGET +involve leaving addresses that point to the forgotten code that are not +themselves forgotten. This can occur if you set a deferred system word +to your word then FORGET your word. The system word which is below your +word in the dictionary is pointing up to code that no longer exists. It +will probably crash if called. (See discussion of DEFER below.) Another +problem is if your code allocates memory, opens files, or opens windows. +If your code is forgotten you may have no way to free or close these +thing. You could also have a problems if you add addresses from your +code to a table that is below your code. This might be a jump table or +data table. + +Since this is a common problem we have provided a tool for handling it. +If you have some code that you know could potentially cause a problem if +forgotten, then write a cleanup word that will eliminate the problem. +This word could UNdefer words, free memory, etc. Then tell the system to +call this word if the code is forgotten. Here is how: + +- : MY.CLEANUP ( -- , do whatever ) + MY-MEM @ FREE DROP + 0 MY-MEM ! + ; + IF.FORGOTTEN MY.CLEANUP + +IF.FORGOTTEN creates a linked list node containing your CFA that is +checked by FORGET. Any nodes that end up above HERE (the Forth pointer +to the top of the dictionary) after FORGET is done are executed. + +### []{#Customising-FORGET}Customising FORGET with \[FORGET\] + +Sometimes, you may need to extend the way that FORGET works. FORGET is +not deferred, however, because that could cause some real problems. +Instead, you can define a new version of \[FORGET\] which is searched +for and executed by FORGET. You MUST call \[FORGET\] from your program +or FORGET will not actually FORGET. Here is an example. + +- : [FORGET] ( -- , my version ) + ." Change things around!" CR + [FORGET] ( must be called ) + ." Now put them back!" CR + ; + : FOO ." Hello!" ; + FORGET FOO ( Will print "Change things around!", etc.) + +This is recommended over redefining FORGET because words like ANEW that +call FORGET will now pick up your changes. + +### []{#Smart-Conditionals}Smart Conditionals + +In pForth, you can use IF THEN DO LOOP and other conditionals outside of +colon definitions. PForth will switch temporarily into the compile +state, then automatically execute the conditional code. (Thank you Mitch +Bradley) For example, just enter this at the keyboard. + +- 10 0 DO I . LOOP + +### []{#Development-Tools}Development Tools + +#### []{#WORDS.LIKE}WORDS.LIKE + +If you cannot remember the exact name of a word, you can use WORDS.LIKE +to search the dictionary for all words that contain a substring. For an +example, enter: + +- WORDS.LIKE FOR + WORDS.LIKE EMIT + +#### []{#FILEQ}FILE? + +You can use FILE? to find out what file a word was compiled from. If a +word was defined in multiple files then it will list each file. The +execution token of each definition of the word is listed on the same +line. + +- FILE? IF + FILE? AUTO.INIT + +#### []{#SEE}SEE + +You can use SEE to \"disassemble\" a word in the pForth dictionary. SEE +will attempt to print out Forth source in a form that is similar to the +source code. SEE will give you some idea of how the word was defined but +is not perfect. Certain compiler words, like BEGIN and LITERAL, are +difficult to disassemble and may not print properly. For an example, +enter: + +- SEE SPACES + SEE WORDS + +#### []{#single-step-trace}Single Step Trace and Debug + +It is often useful to proceed step by step through your code when +debugging. PForth provides a simple single step trace facility for this +purpose. Here is an example of using TRACE to debug a simple program. +Enter the following program:\ + + +- : SQUARE ( n -- n**2 ) + DUP * + ; + : TSQ ( n -- , test square ) + ." Square of " DUP . + ." is " SQUARE . CR + ; + +Even though this program should work, let\'s pretend it doesn\'t and try +to debug it. Enter: + +- 7 TRACE TSQ + +You should see: + +- 7 trace tsq + << TSQ +0 <10:1> 7 || (.") Square of " >> ok + +The \"TSQ +0\" means that you are about to execute code at an offset of +\"+0\" from the beginning of TSQ. The \<10:1\> means that we are in +base 10, and that there is 1 item on the stack, which is shown to be +\"7\". The (.\") is the word that is about to be executed. (.\") is the +word that is compiled when use use .\". Now to single step, enter: + +- s + +You should see: + +- Square of + << TSQ +16 <10:1> 7 || DUP >> ok + +The \"Square os\" was printed by (.\"). We can step multiple times using +the \"sm\" command. Enter: + +- 3 sm + +You should see: + +- << TSQ +20 <10:2> 7 7 || . >> 7 + << TSQ +24 <10:1> 7 || (.") is " >> is + << TSQ +32 <10:1> 7 || SQUARE >> ok + +The \"7\" after the \"\>\>\" was printed by the . word. If we entered +\"s\", we would step over the SQUARE word. If we want to dive down into +SQUARE, we can enter: + +- sd + +You should see: + +- << SQUARE +0 <10:1> 7 || DUP >> ok + +To step once in SQUARE, enter: + +- s + +You should see: + +- << SQUARE +4 <10:2> 7 7 || * >> ok + +To go to the end of the current word, enter: + +- g + +You should see: + +- << SQUARE +8 <10:1> 49 || EXIT >> + << TSQ +36 <10:1> 49 || . >> ok + +EXIT is compiled at the end of every Forth word. For more information on +TRACE, enter TRACE.HELP: + +- TRACE ( i*x <name> -- , setup trace for Forth word ) + S ( -- , step over ) + SM ( many -- , step over many times ) + SD ( -- , step down ) + G ( -- , go to end of word ) + GD ( n -- , go down N levels from current level, + stop at end of this level ) + +### []{#Conditional-Compilation}Conditional Compilation \[IF\] \[ELSE\] \[THEN\] + +PForth supports conditional compilation words similar to \'C\'\'s #if, +#else, and #endif. + +\[IF\] ( flag \-- , if true, skip to \[ELSE\] or \[THEN\] ) + +\[ELSE\] ( \-- , skip to \[THEN\] ) + +\[THEN\] ( \-- , noop, used to terminate \[IF\] and \[ELSE\] section ) + +For example: + +- TRUE constant USE_FRENCH + + USE_FRENCH [IF] + : WELCOME ." Bienvenue!" cr ; + [ELSE] + : WELCOME ." Welcome!" cr ; + [THEN] + +Here is how to conditionally compile within a colon definition by using +\[ and \]. + +- : DOIT ( -- ) + START.REACTOR + IF + [ USE_FRENCH [IF] ] ." Zut alors!" + [ [ELSE] ] ." Uh oh!" + [THEN] + THEN cr + ; + +### []{#Miscellaneous-Words}Miscellaneous Handy Words + +.HEX ( n \-- , print N as hex number ) + +CHOOSE ( n \-- rand , select random number between 0 and N-1 ) + +MAP ( \-- , print dictionary information ) + +### []{#Local-Variables}Local Variables { foo \--} + +In a complicated Forth word it is sometimes hard to keep track of where +things are on the stack. If you find you are doing a lot of stack +operations like DUP SWAP ROT PICK etc. then you may want to use local +variables. They can greatly simplify your code. You can declare local +variables for a word using a syntax similar to the stack diagram. These +variables will only be accessible within that word. Thus they are +\"local\" as opposed to \"global\" like regular variables. Local +variables are self-fetching. They automatically put their values on the +stack when you give their name. You don\'t need to @ the contents. Local +variables do not take up space in the dictionary. They reside on the +return stack where space is made for them as needed. Words written with +them can be reentrant and recursive. + +Consider a word that calculates the difference of two squares, Here are +two ways of writing the same word. + +- : DIFF.SQUARES ( A B -- A*A-B*B ) + DUP * + SWAP DUP * + SWAP - + ; + ( or ) + : DIFF.SQUARES { A B -- A*A-B*B } + A A * + B B * - + ; + 3 2 DIFF.SQUARES ( would return 5 ) + +In the second definition of DIFF.SQUARES the curly bracket \'{\' told +the compiler to start declaring local variables. Two locals were +defined, A and B. The names could be as long as regular Forth words if +desired. The \"\--\" marked the end of the local variable list. When the +word is executed, the values will automatically be pulled from the stack +and placed in the local variables. When a local variable is executed it +places its value on the stack instead of its address. This is called +self-fetching. Since there is no address, you may wonder how you can +store into a local variable. There is a special operator for local +variables that does a store. It looks like -\> and is pronounced \"to\". + +Local variables need not be passed on the stack. You can declare a local +variable by placing it after a \"vertical bar\" ( \| )character. These +are automatically set to zero when created. Here is a simple example +that uses -\> and \| in a word: + +- : SHOW2* + { loc1 | unvar -- , 1 regular, 1 uninitialized } + LOC1 2* -> UNVAR + (set unver to 2*LOC1 ) + UNVAR . ( print UNVAR ) + ; + 3 SHOW2* ( pass only 1 parameter, prints 6 ) + +Since local variable often used as counters or accumulators, we have a +special operator for adding to a local variable It is +-\> which is +pronounced \"plus to\". These next two lines are functionally equivalent +but the second line is faster and smaller: + +- ACCUM 10 + -> ACCUM + 10 +-> ACCUM + +If you name a local variable the same as a Forth word in the dictionary, +eg. INDEX or COUNT, you will be given a warning message. The local +variable will still work but one could easily get confused so we warn +you about this. Other errors that can occur include, missing a closing +\'}\', missing \'\--\', or having too many local variables. + +### []{#C-Structures}\'C\' like Structures. :STRUCT + +You can define \'C\' like data structures in pForth using :STRUCT. For +example: + +- :STRUCT SONG + LONG SONG_NUMNOTES \ define 32 bit structure member named SONG_NUMNOTES + SHORT SONG_SECONDS \ define 16 bit structure member + BYTE SONG_QUALITY \ define 8 bit member + LONG SONG_NUMBYTES \ auto aligns after SHORT or BYTE + RPTR SONG_DATA \ relocatable pointer to data + ;STRUCT + + SONG HAPPY \ define a song structure called happy + + 400 HAPPY S! SONG_NUMNOTES \ set number of notes to 400 + 17 HAPPY S! SONG_SECONDS \ S! works with all size members + + CREATE SONG-DATA 23 , 17 , 19 , 27 , + SONG-DATA HAPPY S! SONG_DATA \ store pointer in relocatable form + + HAPPY DST SONG \ dump HAPPY as a SONG structure + + HAPPY S@ SONG_NUMNOTES . \ fetch numnotes and print + +See the file \"c_struct.fth\" for more information. + +### []{#Vectorred-Execution}Vectorred Execution - DEFER + +Using DEFER for vectored words. In Forth and other languages you can +save the address of a function in a variable. You can later fetch from +that variable and execute the function it points to.This is called +vectored execution. PForth provides a tool that simplifies this process. +You can define a word using DEFER. This word will contain the execution +token of another Forth function. When you execute the deferred word, it +will execute the function it points to. By changing the contents of this +deferred word, you can change what it will do. There are several words +that support this process. + +DEFER ( \<name\> \-- , define a deferred word ) + +IS ( CFA \<name\> \-- , set the function for a deferred word ) + +WHAT\'S ( \<name\> \-- CFA , return the CFA set by IS ) + +Simple way to see the name of what\'s in a deferred word: + +- - WHAT'S EMIT >NAME ID. + +should print name of current word that\'s in EMIT. + +Here is an example that uses a deferred word. + +- DEFER PRINTIT + ' . IS PRINTIT ( make PRINTIT use . ) + 8 3 + PRINTIT + + : COUNTUP ( -- , call deferred word ) + ." Hit RETURN to stop!" CR + 0 ( first value ) + BEGIN 1+ DUP PRINTIT CR + ?TERMINAL + UNTIL + ; + COUNTUP ( uses simple . ) + + : FANCY.PRINT ( N -- , print in DECIMAL and HEX) + DUP ." DECIMAL = " . + ." , HEX = " .HEX + ; + ' FANCY.PRINT IS PRINTIT ( change printit ) + WHAT'S PRINTIT >NAME ID. ( shows use of WHAT'S ) + 8 3 + PRINTIT + COUNTUP ( notice that it now uses FANCY.PRINT ) + +Many words in the system have been defined using DEFER which means that +we can change how they work without recompiling the entire system. Here +is a partial list of those words + +- ABORT EMIT NUMBER? + +#### Potential Problems with Defer + +Deferred words are very handy to use, however, you must be careful with +them. One problem that can occur is if you initialize a deferred system +more than once. In the below example, suppose we called STUTTER twice. +The first time we would save the original EMIT vector in OLD-EMIT and +put in a new one. The second time we called it we would take our new +function from EMIT and save it in OLD-EMIT overwriting what we had saved +previously. Thus we would lose the original vector for EMIT . You can +avoid this if you check to see whether you have already done the defer. +Here\'s an example of this technique. + +- DEFER OLD-EMIT + ' QUIT IS OLD-EMIT ( set to known value ) + : EEMMIITT ( char --- , our fun EMIT ) + DUP OLD-EMIT OLD-EMIT + ; + : STUTTER ( --- ) + WHAT'S OLD-EMIT 'C QUIT = ( still the same? ) + IF ( this must be the first time ) + WHAT'S EMIT ( get the current value of EMIT ) + IS OLD-EMIT ( save this value in OLD-EMIT ) + 'C EEMMIITT IS EMIT + ELSE ." Attempt to STUTTER twice!" CR + THEN + ; + : STOP-IT! ( --- ) + WHAT'S OLD-EMIT ' QUIT = + IF ." STUTTER not installed!" CR + + ELSE WHAT'S OLD-EMIT IS EMIT + 'C QUIT IS OLD-EMIT + ( reset to show termination ) + THEN + ; + +In the above example, we could call STUTTER or STOP-IT! as many times as +we want and still be safe. + +Suppose you forget your word that EMIT now calls. As you compile new +code you will overwrite the code that EMIT calls and it will crash +miserably. You must reset any deferred words that call your code before +you FORGET your code. The easiest way to do this is to use the word +IF.FORGOTTEN to specify a cleanup word to be called if you ever FORGET +the code in question. In the above example using EMIT , we could have +said: + +- IF.FORGOTTEN STOP-IT! + +### []{#Floating-Point}Floating Point + +PForth supports the FLOAT word set and much of the FLOATEXT word set as +a compile time option. You can select single or double precision as the +default by changing the typedef of PF_FLOAT. + +PForth has several options for floating point output. + +FS. ( r -f- , prints in scientific/exponential format ) + +FE. ( r -f- , prints in engineering format, exponent if multiple of 3 ) + +FG. ( r -f- , prints in normal or exponential format depending on size ) + +F. ( r -f- , as defined by the standard ) + + + +Here is an example of output from each word for a number ranging from +large to very small. + + FS. FE. FG. F. + 1.234000e+12 1.234000e+12 1.234e+12 1234000000000. + 1.234000e+11 123.4000e+09 1.234e+11 123400000000. + 1.234000e+10 12.34000e+09 1.234e+10 12340000000. + 1.234000e+09 1.234000e+09 1.234e+09 1234000000. + 1.234000e+08 123.4000e+06 1.234e+08 123400000. + 1.234000e+07 12.34000e+06 1.234e+07 12340000. + 1.234000e+06 1.234000e+06 1234000. 1234000. + 1.234000e+05 123.4000e+03 123400. 123400.0 + 1.234000e+04 12.34000e+03 12340. 12340.00 + 1.234000e+03 1.234000e+03 1234. 1234.000 + 1.234000e+02 123.4000e+00 123.4 123.4000 + 1.234000e+01 12.34000e+00 12.34 12.34000 + 1.234000e+00 1.234000e+00 1.234 1.234000 + 1.234000e-01 123.4000e-03 0.1234 0.1234000 + 1.234000e-02 12.34000e-03 0.01234 0.0123400 + 1.234000e-03 1.234000e-03 0.001234 0.0012340 + 1.234000e-04 123.4000e-06 0.0001234 0.0001234 + 1.234000e-05 12.34000e-06 1.234e-05 0.0000123 + 1.234000e-06 1.234000e-06 1.234e-06 0.0000012 + 1.234000e-07 123.4000e-09 1.234e-07 0.0000001 + 1.234000e-08 12.34000e-09 1.234e-08 0.0000000 + 1.234000e-09 1.234000e-09 1.234e-09 0.0000000 + 1.234000e-10 123.4000e-12 1.234e-10 0.0000000 + 1.234000e-11 12.34000e-12 1.234e-11 0.0000000 + + 1.234568e+12 1.234568e+12 1.234568e+12 1234567890000. + 1.234568e+11 123.4568e+09 1.234568e+11 123456789000. + 1.234568e+10 12.34568e+09 1.234568e+10 12345678900. + 1.234568e+09 1.234568e+09 1.234568e+09 1234567890. + 1.234568e+08 123.4568e+06 1.234568e+08 123456789. + 1.234568e+07 12.34568e+06 1.234568e+07 12345679. + 1.234568e+06 1.234568e+06 1234568. 1234568. + 1.234568e+05 123.4568e+03 123456.8 123456.8 + 1.234568e+04 12.34568e+03 12345.68 12345.68 + 1.234568e+03 1.234568e+03 1234.568 1234.568 + 1.234568e+02 123.4568e+00 123.4568 123.4568 + 1.234568e+01 12.34568e+00 12.34568 12.34568 + 1.234568e+00 1.234568e+00 1.234568 1.234568 + 1.234568e-01 123.4568e-03 0.1234568 0.1234568 + 1.234568e-02 12.34568e-03 0.01234568 0.0123456 + 1.234568e-03 1.234568e-03 0.001234568 0.0012345 + 1.234568e-04 123.4568e-06 0.0001234568 0.0001234 + 1.234568e-05 12.34568e-06 1.234568e-05 0.0000123 + 1.234568e-06 1.234568e-06 1.234568e-06 0.0000012 + 1.234568e-07 123.4568e-09 1.234568e-07 0.0000001 + 1.234568e-08 12.34568e-09 1.234568e-08 0.0000000 + 1.234568e-09 1.234568e-09 1.234568e-09 0.0000000 + 1.234568e-10 123.4568e-12 1.234568e-10 0.0000000 + 1.234568e-11 12.34568e-12 1.234568e-11 0.0000000 + +## []{#pForth-Design}pForth Design + +### []{#C-kernel}\'C\' kernel + +The pForth kernel is written in \'C\' for portability. The inner +interpreter is implemented in the function ExecuteToken() which is in +pf_inner.c. + +- void pfExecuteToken( ExecToken XT ); + +It is passed an execution token the same as EXECUTE would accept. It +handles threading of secondaries and also has a large switch() case +statement to interpret primitives. It is in one huge routine to take +advantage of register variables, and to reduce calling overhead. +Hopefully, your compiler will optimise the switch() statement into a +jump table so it will run fast. + +### []{#Dictionary-Structures}Dictionary Structures + +This Forth supports multiple dictionaries. Each dictionary consists of a +header segment and a seperate code segment. The header segment contains +link fields and names. The code segment contains tokens and data. The +headers, as well as some entire dictionaries such as the compiler +support words, can be discarded when creating a stand-alone app. + +\[NOT IMPLEMENTED\] Dictionaries can be split so that the compile time +words can be placed above the main dictionary. Thus they can use the +same relative addressing but be discarded when turnkeying. + +Execution tokens are either an index of a primitive ( n \< +NUM_PRIMITIVES), or the offset of a secondary in the code segment. ( n +\>= NUM_PRIMITIVES ) + +The NAME HEADER portion of the dictionary contains a structure for each +named word in the dictionary. It contains the following fields: + +- bytes + 4 Link Field = relative address of previous name header + 4 Code Pointer = relative address of corresponding code + n Name Field = name as counted string Headers are quad byte aligned. + +The CODE portion of the dictionary consists of the following structures: + +#### Primitive + +No Forth code. \'C\' code in \"pf_inner.c\". + +#### Secondary + +- 4*n Parameter Field containing execution tokens + 4 ID_NEXT = 0 terminates secondary + +#### CREATE DOES\> + +- 4 ID_CREATE_P token + 4 Token for optional DOES> code, OR ID_NEXT = 0 + 4 ID_NEXT = 0 + n Body = arbitrary data + +#### Deferred Word + +- 4 ID_DEFER_P same action as ID_NOOP, identifies deferred words + 4 Execution Token of word to execute. + 4 ID_NEXT = 0 + +#### Call to custom \'C\' function. + +- 4 ID_CALL_C + 4 Pack C Call Info Bits + + - 0-15 = Function Index Bits + 16-23 = FunctionTable Index (Unused) Bits + 24-30 = NumParams Bit + 31 = 1 if function returns value + + <!-- --> + + 4 ID_NEXT = 0 + +------------------------------------------------------------------------ + +## []{#Compiling-pForth}Compiling pForth + +A makefile is supplied that will help you compile pForth for your +environment. You can customize the build by setting various compiler +options. + +### []{#Compiler-Options}Compiler Options + +There are several versions of PForth that can be built. By default, the +full kernel will be built. For custom builds, define the following +options in the Makefile before compiling the \'C\' code: + +PF_DEFAULT_DICTIONARY=\"filename\" + +> Specify a dictionary to use in place of the default \"pforth.dic\", +> for example \"/usr/lib/pforth/pforth.dic\". + +PF_NO_INIT + +- Don\'t compile the code used to initially build the dictionary. This + can be used to save space if you already have a prebuilt dictionary. + +PF_NO_SHELL + +- Don\'t compile the outer interpreter and Forth compiler. This can be + used with Cloned dictionaries. + +PF_NO_MALLOC + +- Replace malloc() and free() function with pForth\'s own version. See + pf_mem.c for more details. + +PF_USER_MALLOC=\'\"filename.h\"\' + +- Replace malloc() and free() function with users custom version. See + pf_mem.h for details. + +PF_MEM_POOL_SIZE=numbytes + +- Size of array in bytes used by pForth custom allocator. + +PF_NO_GLOBAL_INIT + +- Define this if you want pForth to not rely on initialization of global + variables by the loader. This may be required for some embedded + systems that may not have a fully functioning loader. Take a look in + \"pfcustom.c\" for an example of its use. + +PF_USER_INC1=\'\"filename.h\"\' + +- File to include BEFORE other include files. Generally set to host + dependent files such as \"pf_mac.h\". + +PF_USER_INC2=\'\"filename.h\"\' + +- File to include AFTER other include files. Generally used to #undef + and re#define symbols. See \"pf_win32.h\" for an example. + +PF_NO_CLIB + +- Replace \'C\' lib calls like toupper and memcpy with pForth\'s own + version. This is useful for embedded systems. + +PF_USER_CLIB=\'\"filename.h\"\' + +- Rreplace \'C\' lib calls like toupper and memcpy with users custom + version. See pf_clib.h for details. + +PF_NO_FILEIO + +- System does not support standard file I/O so stub it out. Setting this + flag will automatically set PF_STATIC_DIC. + +PF_USER_CHARIO=\'\"filename.h\"\' + +- Replace stdio terminal calls like getchar() and putchar() with users + custom version. See pf_io.h for details. + +PF_USER_FILEIO=\'\"filename.h\"\' + +- Replace stdio file calls like fopen and fread with users custom + version. See pf_io.h for details. + +PF_USER_FLOAT=\'\"filename.h\"\' + +- Replace floating point math calls like sin and pow with users custom + version. Also defines PF_FLOAT. + +PF_USER_INIT=MyInit() + +- Call a user defined initialization function that returns a negative + error code if it fails. + +PF_USER_TERM=MyTerm() + +- Call a user defined void termination function. + +PF_STATIC_DIC + +- Compile in static dictionary instead of loading dictionary. from file. + Use \"utils/savedicd.fth\" to save a dictionary as \'C\' source code + in a file called \"pfdicdat.h\". + +PF_SUPPORT_FP + +- Compile ANSI floating point support. + +### []{#Building-pForth-Hosts}Building pForth on Supported Hosts + +To build on UNIX, do nothing, system will default to \"pf_unix.h\". + +To build on Macintosh: + +- -DPF_USER_INC1='"pf_mac.h"' + +To build on PCs: + +- -DPF_USER_INC2='"pf_win32.h"' + +To build a system that only runs turnkey or cloned binaries: + +- -DPF_NO_INIT -DPF_NO_SHELL + +### []{#Compiling-Embedded}Compiling for Embedded Systems + +You may want to create a version of pForth that can be run on a small +system that does not support file I/O. This is useful when bringing up +new computer systems. On UNIX systems, you can use the supplied gmake +target. Simply enter: + +- gmake pfemb + +For other systems, here are the steps to create an embedded pForth. + +1. Determine whether your target system has a different endian-ness + than your host system. If the address of a long word is the address + of the most significant byte, then it is \"big endian\". Examples of + big endian processors are Sparc, Motorola 680x0 and PowerPC60x. If + the address of a long word is the address of the least significant + byte, then it is \"Little Endian\". Examples of little endian + processors are Intel 8088 and derivatives such as the Intel Pentium, + X86. ARM processors can be configured as either big or little + endian. +2. If your target system has a different endian-ness than your host + system, then you must compile a version of pForth for your host that + matches the target. Rebuild pForth with either PF_BIG_ENDIAN_DIC or + PF_LITTLE_ENDIAN_DIC defined. You will need to rebuild pforth.dic + as well as the executable Forth. If you do not specify one of these + variables, then the dictionary will match the native endian-ness of + the host processor. +3. Execute pForth. Notice the message regarding the endian-ness of the + dictionary. +4. Compile your custom Forth words on the host development system. +5. Compile the pForth utulity \"utils/savedicd.fth\". +6. Enter in pForth: SDAD +7. SDAD will generate a file called \"pfdicdat.h\" that contains your + dictionary in source code form. +8. Rewrite the character primitives sdTerminalOut(), sdTerminalIn() and + sdTerminalFlush() defined in pf_io.h to use your new computers + communications port. +9. Write a \"user_chario.h\" file based on the API defined in + \"pf_io.h\". +10. Compile a new version of pForth for your target machine with the + following options: + 1. -DPF_NO_INIT -DPF_NO_MALLOC -DPF_NO_FILEIO \ + -DPF_USER_CHARIO="user_chario.h" \ + -DPF_NO_CLIB -DPF_STATIC_DIC +11. The file \"pfdicdat.h\" will be compiled into this executable and + your dictionary will thus be included in the pForth executable as a + static array. +12. Burn a ROM with your new pForth and run it on your target machine. +13. If you compiled a version of pForth with different endian-ness than + your host system, do not use it for daily operation because it will + be much slower than a native version. + +### []{#Link-Custom-C}Linking with Custom \'C\' Functions + +You can call the pForth interpreter as an embedded tool in a \'C\' +application. For an example of this, see the file pf_main.c. This +application does nothing but load the dictionary and call the pForth +interpreter. + +You can call \'C\' from pForth by adding your own custom \'C\' functions +to a dispatch table, and then adding Forth words to the dictionary that +call those functions. See the file \"pfcustom.c\" for more information. + +### []{#Testing-pForth}Testing your Compiled pForth + +Once you have compiled pForth, you can test it using the small +verification suite we provide. The first test you should run was +written by John Hayes at John Hopkins University. Enter: + +- pforth + include tester.fth + include coretest.fth + bye + +The output will be self explanatory. There are also a number of tests +that I have added that print the number of successes and failures. +Enter: + +- pforth t_corex.fth + pforth t_locals.fth + pforth t_strings.fth + pforth t_floats.ft + +Note that t_corex.fth reveals an expected error because SAVE-INPUT is +not fully implemented. (FIXME)\ + +------------------------------------------------------------------------ + +\ +PForth source code is freely available and is in the public domain. + +Back to [pForth Home Page](../pforth)\ +::: + +::: {#footer} +\(C\) 1997-2015 Mobileer Inc - All Rights Reserved - [Contact +Us](/contacts.php) +::: +::::::::::::: diff --git a/forth/pf_tut.md b/forth/pf_tut.md new file mode 100644 index 0000000..5b4d608 --- /dev/null +++ b/forth/pf_tut.md @@ -0,0 +1,1345 @@ +::::::::::::: {#container} +:::::: {#header} +::: {#leftheader} +[{width="200" height="100" +border="0"}](/) +::: + +::: {#rightheader} +::: + +::: {#midheader} +# SoftSynth + +## \... music and computers \... +::: + +\ +:::::: + +:::: {#leftside} +::: {#leftside_inner} +- [Home](/index.php) +- [Products](/products.php) +- [JSyn](/jsyn/index.php) +- [Syntona](/syntona/index.php) +- [pForth](/pforth/index.php) +- [Music](/music/index.php) +- [Info](/info/index.php) +- [News](/news/index.php) +- [Links](/links/index.php) +- [Contact Us](/contacts.php) +- [About Us](/aboutus.php) +::: +:::: + +:::: {#rightside} +::: {#rightside_inner} +### Projects + + --------------------------------------------------------------------------------------------------- + [JSyn](/jsyn/) - modular synthesis API for Java. + [JMSL](https://www.algomusic.com/jmsl/){target="_blank"} - Java Music Specification Language + [PortAudio](https://www.portaudio.com/){target="_blank"} - cross platform audio I/O API for \'C\' + --------------------------------------------------------------------------------------------------- +::: +:::: + +::: {#content} +[pForth](/pforth/index.php) + : [GitHub](https://github.com/philburk/pforth/) + \| [Tutorial]{.current_link} \| [Reference](/pforth/pf_ref.php) + \| [Links](/forthlinks.php) + +------------------------------------------------------------------------ + +# Forth Tutorial + +------------------------------------------------------------------------ + +Translations: +[Chinese](http://vision.twbbs.org/%7Eletoh/forth/pf_tuttw.html){target="_blank"} +by +[Letoh](http://vision.twbbs.org/%7Eletoh/blog/?page_id=169){target="_blank"} + +by [Phil Burk](http://www.softsynth.com/philburk.html) of +[SoftSynth.com](http://www.softsynth.com) + +## Table of Contents + +- [Forth Syntax](#Forth%20Syntax) +- [Stack Manipulation](#The%20Stack) +- [Arithmetic](#Arithmetic) +- [Defining a New Word](#Defining%20a%20New%20Word) +- [More Arithmetic](#More%20Arithmetic) + - [Arithmetic Overflow](#Arithmetic%20Overflow) + - [Convert Algebraic Expressions to + Forth](#Convert%20Algebraic%20Expressions%20to%20Forth) +- [Character Input and Output](#Character%20Input%20and%20Output) +- [Compiling from Files](#Compiling%20from%20Files) +- [Variables](#Variables) +- [Constants](#Constants) +- [Logical Operators](#Logical%20Operators) +- [Conditionals - IF ELSE THEN + CASE](#Conditionals%20-%20IF%20ELSE%20THEN%20CASE) +- [Loops](#Loops) +- [Text Input and Output](#Text%20Input%20and%20Output) +- [Changing Numeric Base](#Changing%20Numeric%20Base) +- [Answers to Problems](#Answers%20to%20Problems) + +The intent of this tutorial is to provide a series of experiments that +will introduce you to the major concepts of Forth. It is only a starting +point. Feel free to deviate from the sequences I provide. A free form +investigation that is based on your curiosity is probably the best way +to learn any language. Forth is especially well adapted to this type of +learning. + +This tutorial is written for the PForth implementation of the ANS Forth +standard. I have tried to restrict this tutorial to words that are part +of the ANS standard but some PForth specific words may have crept in. + +In the tutorials, I will print the things you need to type in upper +case, and indent them. You can enter them in upper or lower case. At the +end of each line, press the RETURN (or ENTER) key; this causes Forth to +interpret what you\'ve entered. + +## []{#Forth Syntax}Forth Syntax + +Forth has one of the simplest syntaxes of any computer language. The +syntax can be stated as follows, \"**Forth code is a bunch of words with +spaces between them.**\" This is even simpler than English! Each *word* +is equivalent to a function or subroutine in a language like \'C\'. They +are executed in the order they appear in the code. The following +statement, for example, could appear in a Forth program: + +- WAKE.UP EAT.BREAKFAST WORK EAT.DINNER PLAY SLEEP + +Notice that WAKE.UP has a dot between the WAKE and UP. The dot has no +particular meaning to the Forth compiler. I simply used a dot to connect +the two words together to make one word. Forth word names can have any +combination of letters, numbers, or punctuation. We will encounter words +with names like: + +- ." #S SWAP ! @ ACCEPT . * + +They are all called *words*. The word **\$%%-GL7OP** is a legal Forth +name, although not a very good one. It is up to the programmer to name +words in a sensible manner. + +Now it is time to run your Forth and begin experimenting. Please consult +the manual for your Forth for instructions on how to run it. + +## []{#The Stack}Stack Manipulation + +The Forth language is based on the concept of a *stack*. Imagine a stack +of blocks with numbers on them. You can add or remove numbers from the +top of the stack. You can also rearrange the order of the numbers. Forth +uses several stacks. The *DataStack* is the one used for passing data +between Forth words so we will concentrate our attention there. The +*Return Stack* is another Forth stack that is primarily for internal +system use. In this tutorial, when we refer to the \"stack,\" we will be +referring to the Data Stack. + +The stack is initially empty. To put some numbers on the stack, enter: + +- 23 7 9182 + +Let\'s now print the number on top of the stack using the Forth word \' +**.** \', which is pronounced \" dot \". This is a hard word to write +about in a manual because it is a single period. + +Enter: **. ** + +You should see the last number you entered, 9182 , printed. Forth has a +very handy word for showing you what\'s on the stack. It is **.S** , +which is pronounced \"dot S\". The name was constructed from \"dot\" for +print, and \"S\" for stack. (PForth will automatically print the stack +after every line if the TRACE-STACK variable is set to TRUE.) If you +enter: + +- .S + +you will see your numbers in a list. The number at the far right is the +one on top of the stack. + +You will notice that the 9182 is not on the stack. The word \' . \' +removes the number on top of the stack before printing it. In contrast, +\' .S \' leaves the stack untouched. + +We have a way of documenting the effect of words on the stack with a +*stack diagram*. A stack diagram is contained in parentheses. In Forth, +the parentheses indicate a comment. In the examples that follow, you do +not need to type in the comments. When you are programming, of course, +we encourage the use of comments and stack diagrams to make your code +more readable. In this manual, we often indicate stack diagrams in +**bold text** like the one that follows. Do not type these in. The stack +diagram for a word like \' . \' would be: + +**`. ( N -- , print number on top of stack )`** + +The symbols to the left of \-- describe the parameters that a word +expects to process. In this example, N stands for any integer number. To +the right of \--, up to the comma, is a description of the stack +parameters when the word is finished, in this case there are none +because \'dot\' \"eats\" the N that was passed in. (Note that the stack +descriptions are not necessary, but they are a great help when learning +other peoples programs.) + +The text following the comma is an English description of the word. You +will note that after the \-- , N is gone. You may be concerned about the +fact that there were other numbers on the stack, namely 23 and 7 . The +stack diagram, however, only describes the portion of the stack that is +affected by the word. For a more detailed description of the stack +diagrams, there is a special section on them in this manual right before +the main glossary section. + +Between examples, you will probably want to clear the stack. If you +enter **0SP**, pronounced \"zero S P\", then the stack will be cleared. + +Since the stack is central to Forth, it is important to be able to alter +the stack easily. Let\'s look at some more words that manipulate the +stack. Enter: + +- 0SP .S \ That's a 'zero' 0, not an 'oh' O. + 777 DUP .S + +You will notice that there are two copies of 777 on the stack. The word +**DUP** duplicates the top item on the stack. This is useful when you +want to use the number on top of the stack and still have a copy. The +stack diagram for DUP would be: + +**`DUP ( n -- n n , DUPlicate top of stack )`** + +Another useful word, is **SWAP**. Enter: + +- 0SP + 23 7 .S + SWAP .S + SWAP .S + +The stack diagram for SWAP would be: + +**`SWAP ( a b -- b a , swap top two items on stack )`** + +Now enter: + +- OVER .S + OVER .S + +The word **OVER** causes a copy of the second item on the stack to +leapfrog over the first. It\'s stack diagram would be: + +**`OVER ( a b -- a b a , copy second item on stack )`** + +Here is another commonly used Forth word: + +**`DROP ( a -- , remove item from the stack )`** + +Can you guess what we will see if we enter: + +- 0SP 11 22 .S + DROP .S + +Another handy word for manipulating the stack is **ROT**. Enter: + +- 0SP + 11 22 33 44 .S + ROT .S + +The stack diagram for ROT is, therefore: + +**`ROT ( a b c -- b c a , ROTate third item to top ) `** + +You have now learned the more important stack manipulation words. You +will see these in almost every Forth program. I should caution you that +if you see too many stack manipulation words being used in your code +then you may want to reexamine and perhaps reorganize your code. You +will often find that you can avoid excessive stack manipulations by +using *local or global VARIABLES* which will be discussed later. + +If you want to grab any arbitrary item on the stack, use **PICK** . Try +entering: + +- 0SP + 14 13 12 11 10 + 3 PICK . ( prints 13 ) + 0 PICK . ( prints 10 ) + 4 PICK . + +PICK makes a copy of the Nth item on the stack. The numbering starts +with zero, therefore: + +- `0 PICK is equivalent to DUP`\ + `1 PICK is equivalent to OVER` + +**`PICK ( ... v3 v2 v1 v0 N -- ... v3 v2 v1 v0 vN ) `** + +(Warning. The Forth-79 and FIG Forth standards differ from the ANS and +Forth \'83 standard in that their PICK numbering starts with one, not +zero.) + +I have included the stack diagrams for some other useful stack +manipulation words. Try experimenting with them by putting numbers on +the stack and calling them to get a feel for what they do. Again, the +text in parentheses is just a comment and need not be entered. + +**`DROP ( n -- , remove top of stack ) `** + +**`?DUP ( n -- n n | 0 , duplicate only if non-zero, '|' means OR ) `** + +**`-ROT ( a b c -- c a b , rotate top to third position ) `** + +**`2SWAP ( a b c d -- c d a b , swap pairs ) `** + +**`2OVER ( a b c d -- a b c d a b , leapfrog pair ) `** + +**`2DUP ( a b -- a b a b , duplicate pair ) `** + +**`2DROP ( a b -- , remove pair ) `** + +**`NIP ( a b -- b , remove second item from stack ) `** + +**`TUCK ( a b -- b a b , copy top item to third position ) `** + +### []{#Problems - Stack}Problems: + +Start each problem by entering: + +- 0SP 11 22 33 + +Then use the stack manipulation words you have learned to end up with +the following numbers on the stack: + +- 1) 11 33 22 22 + + 2) 22 33 + + 3) 22 33 11 11 22 + + 4) 11 33 22 33 11 + + 5) 33 11 22 11 22 + +[Answers to the problems](#Answers%20to%20Problems) can be found at the +end of this tutorial. + +## []{#Arithmetic}Arithmetic + +Great joy can be derived from simply moving numbers around on a stack. +Eventually, however, you\'ll want to do something useful with them. This +section describes how to perform arithmetic operations in Forth. + +The Forth arithmetic operators work on the numbers currently on top of +the stack. If you want to add the top two numbers together, use the +Forth word **+** , pronounced \"plus\". Enter: + +- 2 3 + . + 2 3 + 10 + . + +This style of expressing arithmetic operations is called *Reverse Polish +Notation,* or *RPN*. It will already be familiar to those of you with HP +calculators. In the following examples, I have put the algebraic +equivalent representation in a comment. + +Some other arithmetic operators are **- \* /** . Enter: + +- 30 5 - . ( 25=30-5 ) + 30 5 / . ( 6=30/5 ) + 30 5 * . ( 150=30*5 ) + 30 5 + 7 / . \ 5=(30+5)/7 + +Some combinations of operations are very common and have been coded in +assembly language for speed. For example, **2\*** is short for 2 \* . +You should use these whenever possible to increase the speed of your +program. These include: + +- 1+ 1- 2+ 2- 2* 2/ + +Try entering: + +- 10 1- . + 7 2* 1+ . ( 15=7*2+1 ) + +One thing that you should be aware of is that when you are doing +division with integers using / , the remainder is lost. Enter: + +- 15 5 / . + 17 5 / . + +This is true in all languages on all computers. Later we will examine +**/MOD** and **MOD** which do give the remainder. + +## []{#Defining a New Word}Defining a New Word + +It\'s now time to write a *small program* in Forth. You can do this by +defining a new word that is a combination of words we have already +learned. Let\'s define and test a new word that takes the average of two +numbers. + +We will make use of two new words, **:** ( \"colon\"), and **;** ( +\"semicolon\") . These words start and end a typical *Forth definition*. +Enter: + +- : AVERAGE ( a b -- avg ) + 2/ ; + +Congratulations. You have just written a Forth program. Let\'s look more +closely at what just happened. The colon told Forth to add a new word to +its list of words. This list is called the Forth dictionary. The name of +the new word will be whatever name follows the colon. Any Forth words +entered after the name will be compiled into the new word. This +continues until the semicolon is reached which finishes the definition. + +Let\'s test this word by entering: + +- 10 20 AVERAGE . ( should print 15 ) + +Once a word has been defined, it can be used to define more words. +Let\'s write a word that tests our word.. Enter: + +- : TEST ( --) 50 60 AVERAGE . ; + TEST + +Try combining some of the words you have learned into new Forth +definitions of your choice. If you promise not to be overwhelmed, you +can get a list of the words that are available for programming by +entering: + +- WORDS + +Don\'t worry, only a small fraction of these will be used directly in +your programs. + +## []{#More Arithmetic}More Arithmetic + +When you need to know the remainder of a divide operation. /MOD will +return the remainder as well as the quotient. the word MOD will only +return the remainder. Enter: + +- 0SP + 53 10 /MOD .S + 0SP + 7 5 MOD .S + +Two other handy words are **MIN** and **MAX** . They accept two numbers +and return the MINimum or MAXimum value respectively. Try entering the +following: + +- 56 34 MAX . + 56 34 MIN . + -17 0 MIN . + +Some other useful words are: + +**`ABS ( n -- abs(n) , absolute value of n ) `** + +**`NEGATE ( n -- -n , negate value, faster then -1 * ) `** + +**`LSHIFT ( n c -- n<<c , left shift of n ) `** + +**`RSHIFT ( n c -- n>>c , logical right shift of n ) `** + +**`ARSHIFT ( n c -- n>>c ) , arithmetic right shift of n ) `** + +ARSHIFT or LSHIFT can be used if you have to multiply quickly by a power +of 2 . A right shift is like doing a divide by 2. This is often faster +than doing a regular multiply or divide. Try entering: + +- : 256* 8 LSHIFT ; + 3 256* . + +### []{#Arithmetic Overflow}Arithmetic Overflow + +If you are having problems with your calculation overflowing the 32-bit +precision of the stack, then you can use **\*/** . This produces an +intermediate result that is 64 bits long. Try the following three +methods of doing the same calculation. Only the one using \*/ will yield +the correct answer, 5197799. + +- 34867312 99154 * 665134 / . + 34867312 665134 / 99154 * . + 34867312 99154 665134 */ . + +#### []{#Convert Algebraic Expressions to Forth}Convert Algebraic Expressions to Forth + +How do we express complex algebraic expressions in Forth? For example: +20 + (3 \* 4) + +To convert this to Forth you must order the operations in the order of +evaluation. In Forth, therefore, this would look like: + +- 3 4 * 20 + + +Evaluation proceeds from left to right in Forth so there is no +ambiguity. Compare the following algebraic expressions and their Forth +equivalents: (Do **not** enter these!) + +- (100+50)/2 ==> 100 50 + 2/ + ((2*7) + (13*5)) ==> 2 7 * 13 5 * + + +If any of these expressions puzzle you, try entering them one word at a +time, while viewing the stack with .S . + +### []{#Problems - Square}Problems: + +Convert the following algebraic expressions to their equivalent Forth +expressions. (Do **not** enter these because they are not Forth code!) + +- (12 * ( 20 - 17 )) + + (1 - ( 4 * (-18) / 6) ) + + ( 6 * 13 ) - ( 4 * 2 * 7 ) + +Use the words you have learned to write these new words: + +- SQUARE ( N -- N*N , calculate square ) + + DIFF.SQUARES ( A B -- A*A-B*B , difference of squares ) + + AVERAGE4 ( A B C D -- [A+B+C+D]/4 ) + + HMS>SECONDS ( HOURS MINUTES SECONDS -- TOTAL-SECONDS , convert ) + +[Answers to the problems](#Answers%20to%20Problems) can be found at the +end of this tutorial. + +## []{#Character Input and Output}Character Input and Output + +The numbers on top of the stack can represent anything. The top number +might be how many blue whales are left on Earth or your weight in +kilograms. It can also be an ASCII character. Try entering the +following: + +- 72 EMIT 105 EMIT + +You should see the word \"Hi\" appear before the OK. The 72 is an ASCII +\'H\' and 105 is an \'i\'. EMIT takes the number on the stack and +outputs it as a character. If you want to find the ASCII value for any +character, you can use the word ASCII . Enter: + +- CHAR W . + CHAR % DUP . EMIT + CHAR A DUP . + 32 + EMIT + +Here is a complete [ASCII chart](http://www.asciitable.com/). + +Notice that the word CHAR is a bit unusual because its input comes not +from the stack, but from the following text. In a stack diagram, we +represent that by putting the input in angle brackets, \<input\>. Here +is the stack diagram for CHAR. + +**`CHAR ( <char> -- char , get ASCII value of a character ) `** + +Using EMIT to output character strings would be very tedious. Luckily +there is a better way. Enter: + +- : TOFU ." Yummy bean curd!" ; + TOFU + +The word **.\"** , pronounced \"dot quote\", will take everything up to +the next quotation mark and print it to the screen. Make sure you leave +a space after the first quotation mark. When you want to have text begin +on a new line, you can issue a carriage return using the word **CR** . +Enter: + +- : SPROUTS ." Miniature vegetables." ; + : MENU + CR TOFU CR SPROUTS CR + ; + MENU + +You can emit a blank space with **SPACE** . A number of spaces can be +output with SPACES . Enter: + +- CR TOFU SPROUTS + CR TOFU SPACE SPROUTS + CR 10 SPACES TOFU CR 20 SPACES SPROUTS + +For character input, Forth uses the word **KEY** which corresponds to +the word EMIT for output. KEY waits for the user to press a key then +leaves its value on the stack. Try the following. + +- : TESTKEY ( -- ) + ." Hit a key: " KEY CR + ." That = " . CR + ; + TESTKEY + +\[Note: On some computers, the input if buffered so you will need to hit +the ENTER key after typing your character.\] + +**`EMIT ( char -- , output character ) `** + +**`KEY ( -- char , input character ) `** + +**`SPACE ( -- , output a space ) `** + +**`SPACES ( n -- , output n spaces ) `** + +**`CHAR ( <char> -- char , convert to ASCII ) `** + +**`CR ( -- , start new line , carriage return ) `** + +**`." ( -- , output " delimited text ) `** + +## []{#Compiling from Files}Compiling from Files + +PForth can read read from ordinary text files so you can use any editor +that you wish to write your programs. + +### Sample Program + +Enter into your file, the following code. + +- \ Sample Forth Code + \ Author: your name + + : SQUARE ( n -- n*n , square number ) + DUP * + ; + + : TEST.SQUARE ( -- ) + CR ." 7 squared = " + 7 SQUARE . CR + ; + +Now save the file to disk. + +The text following the **\\** character is treated as a comment. This +would be a REM statement in BASIC or a /\*\-\--\*/ in \'C\'. The text in +parentheses is also a comment. + +### Using INCLUDE + +\"INCLUDE\" in Forth means to compile from a file. + +You can compile this file using the INCLUDE command. If you saved your +file as WORK:SAMPLE, then compile it by entering: + +- INCLUDE SAMPLE.FTH + +Forth will compile your file and tell you how many bytes it has added to +the dictionary. To test your word, enter: + +- TEST.SQUARE + +Your two words, SQUARE and TEST.SQUARE are now in the Forth dictionary. +We can now do something that is very unusual in a programming language. +We can \"uncompile\" the code by telling Forth to **FORGET** it. Enter: + +- FORGET SQUARE + +This removes SQUARE and everything that follows it, ie. TEST.SQUARE, +from the dictionary. If you now try to execute TEST.SQUARE it won\'t be +found. + +Now let\'s make some changes to our file and reload it. Go back into the +editor and make the following changes: (1) Change TEST.SQUARE to use 15 +instead of 7 then (2) Add this line right before the definition of +SQUARE: + +- ANEW TASK-SAMPLE.FTH + +Now Save your changes and go back to the Forth window. + +You\'re probably wondering what the line starting with **ANEW** was for. +ANEW is always used at the beginning of a file. It defines a special +marker word in the dictionary before the code. The word typically has +\"TASK-\" as a prefix followed by the name of the file. When you +ReInclude a file, ANEW will automatically FORGET the old code starting +after the ANEW statement. This allows you to Include a file over and +over again without having to manually FORGET the first word. If the code +was not forgotten, the dictionary would eventually fill up. + +If you have a big project that needs lots of files, you can have a file +that will load all the files you need. Sometimes you need some code to +be loaded that may already be loaded. The word **INCLUDE?** will only +load code if it isn\'t already in the dictionary. In this next example, +I assume the file is on the volume WORK: and called SAMPLE. If not, +please substitute the actual name. Enter: + +- FORGET TASK-SAMPLE.FTH + INCLUDE? SQUARE WORK:SAMPLE + INCLUDE? SQUARE WORK:SAMPLE + +Only the first INCLUDE? will result in the file being loaded. + +## []{#Variables}Variables + +Forth does not rely as heavily on the use of variables as other compiled +languages. This is because values normally reside on the stack. There +are situations, of course, where variables are required. To create a +variable, use the word **VARIABLE** as follows: + +- VARIABLE MY-VAR + +This created a variable named MY-VAR . A space in memory is now reserved +to hold its 32-bit value. The word VARIABLE is what\'s known as a +\"defining word\" since it creates new words in the dictionary. Now +enter: + +- MY-VAR . + +The number you see is the address, or location, of the memory that was +reserved for MY-VAR. To store data into memory you use the word **!** , +pronounced \"store\". It looks like an exclamation point, but to a Forth +programmer it is the way to write 32-bit data to memory. To read the +value contained in memory at a given address, use the Forth word **@** , +pronounced \"fetch\". Try entering the following: + +- 513 MY-VAR ! + MY-VAR @ . + +This sets the variable MY-VAR to 513 , then reads the value back and +prints it. The stack diagrams for these words follows: + +**`@ ( address -- value , FETCH value FROM address in memory ) `** + +**`! ( value address -- , STORE value TO address in memory )`** + +**`VARIABLE ( <name> -- , define a 4 byte memory storage location)`** + +A handy word for checking the value of a variable is **?** , pronounced +\"question\". Try entering: + +- MY-VAR ? + +If ? wasn\'t defined, we could define it as: + +- : ? ( address -- , look at variable ) + @ . + ; + +Imagine you are writing a game and you want to keep track of the highest +score. You could keep the highest score in a variable. When you reported +a new score, you could check it aginst the highest score. Try entering +this code in a file as described in the previous section: + +- VARIABLE HIGH-SCORE + + : REPORT.SCORE ( score -- , print out score ) + DUP CR ." Your Score = " . CR + HIGH-SCORE @ MAX ( calculate new high ) + DUP ." Highest Score = " . CR + HIGH-SCORE ! ( update variable ) + ; + +Save the file to disk, then compile this code using the INCLUDE word. +Test your word as follows: + +- 123 REPORT.SCORE + 9845 REPORT.SCORE + 534 REPORT.SCORE + +The Forth words @ and ! work on 32-bit quantities. Some Forths are +\"16-bit\" Forths. They fetch and store 16-bit quantities. Forth has +some words that will work on 8 and 16-bit values. C@ and C! work +characters which are usually for 8-bit bytes. The \'C\' stands for +\"Character\" since ASCII characters are 8-bit numbers. Use W@ and W! +for 16-bit \"Words.\" + +Another useful word is **+!** , pronounced \"plus store.\" It adds a +value to a 32-bit value in memory. Try: + +- 20 MY-VAR ! + 5 MY-VAR +! + MY-VAR @ . + +Forth also provides some other words that are similar to VARIABLE. Look +in the glossary for VALUE and ARRAY. Also look at the section on +\"[local variables](pf_ref.php#Local%20Variables%20%7B%20foo%20--%7D?)\" +which are variables which only exist on the stack while a Forth word is +executing. + +*A word of warning about fetching and storing to memory*: You have now +learned enough about Forth to be dangerous. The operation of a computer +is based on having the right numbers in the right place in memory. You +now know how to write new numbers to any place in memory. Since an +address is just a number, you could, but shouldn\'t, enter: + +- 73 253000 ! ( Do NOT do this. ) + +The 253000 would be treated as an address and you would set that memory +location to 73. I have no idea what will happen after that, maybe +nothing. This would be like firing a rifle through the walls of your +apartment building. You don\'t know who or what you are going to hit. +Since you share memory with other programs including the operating +system, you could easily cause the computer to behave strangely, even +crash. Don\'t let this bother you too much, however. Crashing a +computer, unlike crashing a car, does not hurt the computer. You just +have to reboot. The worst that could happen is that if you crash while +the computer is writing to a disk, you could lose a file. That\'s why we +make backups. This same potential problem exists in any powerful +language, not just Forth. This might be less likely in BASIC, however, +because BASIC protects you from a lot of things, including the danger of +writing powerful programs. + +Another way to get into trouble is to do what\'s called an \"odd address +memory access.\" The 68000 processor arranges words and longwords, 16 +and 32 bit numbers, on even addresses. If you do a **@** or **!** , or +**W@** or **W!** , to an odd address, the 68000 processor will take +exception to this and try to abort. + +Forth gives you some protection from this by trapping this exception and +returning you to the OK prompt. If you really need to access data on an +odd address, check out the words **ODD@** and **ODD!** in the glossary. +**C@** and **C!** work fine on both odd and even addresses. + +## []{#Constants}Constants + +If you have a number that is appearing often in your program, we +recommend that you define it as a \"constant.\" Enter: + +- 128 CONSTANT MAX_CHARS + MAX_CHARS . + +We just defined a word called MAX_CHARS that returns the value on the +stack when it was defined. It cannot be changed unless you edit the +program and recompile. Using **CONSTANT** can improve the readability of +your programs and reduce some bugs. Imagine if you refer to the number +128 very often in your program, say 8 times. Then you decide to change +this number to 256. If you globally change 128 to 256 you might change +something you didn\'t intend to. If you change it by hand you might miss +one, especially if your program occupies more than one file. Using +CONSTANT will make it easy to change. The code that results is equally +as fast and small as putting the numbers in directly. I recommend +defining a constant for almost any number. + +## []{#Logical Operators}Logical Operators + +These next two sections are concerned with decision making. This first +section deals with answering questions like \"Is this value too large?\" +or \"Does the guess match the answer?\". The answers to questions like +these are either TRUE or FALSE. Forth uses a 0 to represent **FALSE** +and a -1 to represent **TRUE**. TRUE and FALSE have been capitalized +because they have been defined as Forth constants. Try entering: + +- 23 71 = . + 18 18 = . + +You will notice that the first line printed a 0, or FALSE, and the +second line a -1, or TRUE. The equal sign in Forth is used as a +question, not a statement. It asks whether the top two items on the +stack are equal. It does not set them equal. There are other questions +that you can ask. Enter: + +- 23 198 < . + 23 198 > . + 254 15 > . + +In California, the drinking age for alcohol is 21. You could write a +simple word now to help bartenders. Enter: + +- : DRINK? ( age -- flag , can this person drink? ) + 20 > + ; + + 20 DRINK? . + 21 DRINK? . + 43 DRINK? . + +The word FLAG in the stack diagram above refers to a logical value. + +Forth provides special words for comparing a number to 0. They are +**0=** **0\>** and **0\<** . Using 0\> is faster than calling 0 and \> +separately. Enter: + +- `23 0> . ( print -1 )`\ + `-23 0> . ( print 0 )`\ + `23 0= . ( print 0 )` + +For more complex decisions, you can use the *Boolean* operators **OR** , +**AND** , and **NOT** . OR returns a TRUE if either one or both of the +top two stack items are true. + +- TRUE TRUE OR . + TRUE FALSE OR . + FALSE FALSE OR . + +AND only returns a TRUE if both of them are true. + +- TRUE TRUE AND . + TRUE FALSE AND . + +NOT reverses the value of the flag on the stack. Enter: + +- TRUE . + TRUE NOT . + +Logical operators can be combined. + +- 56 3 > 56 123 < AND . + 23 45 = 23 23 = OR . + +Here are stack diagrams for some of these words. See the glossary for a +more complete list. + +**`< ( a b -- flag , flag is true if A is less than B )`** + +**`> ( a b -- flag , flag is true if A is greater than B )`** + +**`= ( a b -- flag , flag is true if A is equal to B )`** + +**`0= ( a -- flag , true if a equals zero )`** + +**`OR ( a b -- a||b , perform logical OR of bits in A and B )`** + +**`AND ( a b -- a&b , perform logical AND of bits in A and B )`** + +**`NOT ( flag -- opposite-flag , true if false, false if true )`** + +### []{#Problems - Logical}Problems: + +1\) Write a word called LOWERCASE? that returns TRUE if the number on +top of the stack is an ASCII lowercase character. An ASCII \'a\' is 97 . +An ASCII \'z\' is 122 . Test using the characters \" A \` a q z { \". + +- CHAR A LOWERCASE? . ( should print 0 ) + CHAR a LOWERCASE? . ( should print -1 ) + +[Answers to the problems](#Answers%20to%20Problems) can be found at the +end of this tutorial. + +## []{#Conditionals - IF ELSE THEN CASE}Conditionals - IF ELSE THEN CASE + +You will now use the TRUE and FALSE flags you learned to generate in the +last section. The \"flow of control\" words accept flags from the stack, +and then possibly \"branch\" depending on the value. Enter the following +code. + +- : .L ( flag -- , print logical value ) + IF ." True value on stack!" + ELSE ." False value on stack!" + THEN + ; + + 0 .L + FALSE .L + TRUE .L + 23 7 < .L + +You can see that when a TRUE was on the stack, the first part got +executed. If a FALSE was on the stack, then the first part was skipped, +and the second part was executed. One thing you will find interesting is +that if you enter: + +- 23 .L + +the value on the stack will be treated as true. The flow of control +words consider any value that does not equal zero to be TRUE. + +The **ELSE** word is optional in the **IF\...THEN** construct. Try the +following: + +- : BIGBUCKS? ( amount -- ) + 1000 > + IF ." That's TOO expensive!" + THEN + ; + + 531 BIGBUCKS? + 1021 BIGBUCKS? + +Many Forths also support a **CASE** statement similar to switch() in +\'C\'. Enter: + +- : TESTCASE ( N -- , respond appropriately ) + CASE + 0 OF ." Just a zero!" ENDOF + 1 OF ." All is ONE!" ENDOF + 2 OF WORDS ENDOF + DUP . ." Invalid Input!" + ENDCASE CR + ; + + 0 TESTCASE + 1 TESTCASE + 5 TESTCASE + +See CASE in the glossary for more information. + +### []{#Problems - Conditionals}Problems: + +1\) Write a word called DEDUCT that subtracts a value from a variable +containing your checking account balance. Assume the balance is in +dollars. Print the balance. Print a warning if the balance is negative. + +- VARIABLE ACCOUNT + + : DEDUCT ( n -- , subtract N from balance ) + ????????????????????????????????? ( you fill this in ) + ; + + 300 ACCOUNT ! ( initial funds ) + 40 DEDUCT ( prints 260 ) + 200 DEDUCT ( print 60 ) + 100 DEDUCT ( print -40 and give warning! ) + +[Answers to the problems](#Answers%20to%20Problems) can be found at the +end of this tutorial. + +## []{#Loops}Loops + +Another useful pair of words is **BEGIN\...UNTIL** . These are used to +loop until a given condition is true. Try this: + +- : COUNTDOWN ( N -- ) + BEGIN + DUP . CR ( print number on top of stack ) + 1- DUP 0< ( loop until we go negative ) + UNTIL + ; + + 16 COUNTDOWN + +This word will count down from N to zero. + +If you know how many times you want a loop to execute, you can use the +**DO\...LOOP** construct. Enter: + +- : SPELL + ." ba" + 4 0 DO + ." na" + LOOP + ; + +This will print \"ba\" followed by four occurrences of \"na\". The +ending value is placed on the stack before the beginning value. Be +careful that you don\'t pass the values in reverse. Forth will go \"the +long way around\" which could take awhile. The reason for this order is +to make it easier to pass the loop count into a word on the stack. +Consider the following word for doing character graphics. Enter: + +- : PLOT# ( n -- ) + 0 DO + [CHAR] - EMIT + LOOP CR + ; + + CR 9 PLOT# 37 PLOT# + +If you want to access the loop counter you can use the word I . Here is +a simple word that dumps numbers and their associated ASCII characters. + +- : .ASCII ( end start -- , dump characters ) + DO + CR I . I EMIT + LOOP CR + ; + + 80 64 .ASCII + +If you want to leave a DO LOOP before it finishes, you can use the word +**LEAVE**. Enter: + +- : TEST.LEAVE ( -- , show use of leave ) + 100 0 + DO + I . CR \ print loop index + I 20 > \ is I over 20 + IF + LEAVE + THEN + LOOP + ; + TEST.LEAVE \ will print 0 to 20 + +Please consult the manual to learn about the following words **+LOOP** +and **RETURN** . FIXME + +Another useful looping construct is the **BEGIN WHILE REPEAT** loop. +This allows you to make a test each time through the loop before you +actually do something. The word WHILE will continue looping if the flag +on the stack is True. Enter: + +- : SUM.OF.N ( N -- SUM[N] , calculate sum of N integers ) + 0 \ starting value of SUM + BEGIN + OVER 0> \ Is N greater than zero? + WHILE + OVER + \ add N to sum + SWAP 1- SWAP \ decrement N + REPEAT + SWAP DROP \ get rid on N + ; + + 4 SUM.OF.N \ prints 10 ( 1+2+3+4 ) + +### []{#Problems - Loops}Problems: + +1\) Rewrite SUM.OF.N using a DO LOOP. + +2\) Rewrite SUM.OF.N using BEGIN UNTIL. + +3\) For bonus points, write SUM.OF.N without using any looping or +conditional construct! + +[Answers to the problems](#Answers%20to%20Problems) can be found at the +end of this tutorial. + +## []{#Text Input and Output}Text Input and Output + +You learned earlier how to do single character I/O. This section +concentrates on using strings of characters. You can embed a text string +in your program using S\". Note that you must follow the S\" by one +space. The text string is terminated by an ending \" .Enter: + +- : TEST S" Hello world!" ; + TEST .S + +Note that TEST leaves two numbers on the stack. The first number is the +address of the first character. The second number is the number of +characters in the string. You can print the characters of the string as +follows. + +- TEST DROP \ get rid of number of characters + DUP C@ EMIT \ prints first character, 'H' + CHAR+ DUP C@ EMIT \ prints second character, 'e' + \ and so on + +CHAR+ advances the address to the next character. You can print the +entire string using TYPE. + +- TEST TYPE + TEST 2/ TYPE \ print half of string + +It would be nice if we could simply use a single address to describe a +string and not have to pass the number of characters around. \'C\' does +this by putting a zero at the end of the string to show when it ends. +Forth has a different solution. A text string in Forth consists of a +character count in the first byte, followed immediately by the +characters themselves. This type of character string can be created +using the Forth word C\" , pronounced \'c quote\'. Enter: + +- : T2 C" Greetings Fred" ; + T2 . + +The number that was printed was the address of the start of the string. +It should be a byte that contains the number of characters. Now enter: + +- T2 C@ . + +You should see a 14 printed. Remember that C@ fetches one character/byte +at the address on the stack. You can convert a counted Forth string to +an address and count using COUNT. + +- T2 COUNT .S + TYPE + +The word **COUNT** extracts the number of characters and their starting +address. COUNT will only work with strings of less than 256 characters, +since 255 is the largest number that can be stored in the count byte. +TYPE will, however, work with longer strings since the length is on the +stack. Their stack diagrams follow: + +**`CHAR+ ( address -- address' , add the size of one character )`** + +**`COUNT ( $addr -- addr #bytes , extract string information ) `** + +**`TYPE ( addr #bytes -- , output characters at addr )`** + +The \$addr is the address of a count byte. The dollar sign is often used +to mark words that relate to strings. + +You can easily input a string using the word **ACCEPT**. (You may want +to put these upcoming examples in a file since they are very handy.) The +word **ACCEPT** receives characters from the keyboard and places them at +any specified address. **ACCEPT** takes input characters until a maximum +is reached or an end of line character is entered. **ACCEPT** returns +the number of characters entered. You can write a word for entering +text. Enter: + +- : INPUT$ ( -- $addr ) + PAD 1+ ( leave room for byte count ) + 127 ACCEPT ( recieve a maximum of 127 chars ) + PAD C! ( set byte count ) + PAD ( return address of string ) + ; + + INPUT$ COUNT TYPE + +Enter a string which should then be echoed. You could use this in a +program that writes form letters. + +- : FORM.LETTER ( -- ) + ." Enter customer's name." CR + INPUT$ + CR ." Dear " DUP COUNT TYPE CR + ." Your cup that says " COUNT TYPE + ." is in the mail!" CR + ; + +**`ACCEPT ( addr maxbytes -- numbytes , input text, save at address ) `** + +You can use your word INPUT\$ to write a word that will read a number +from the keyboard. Enter: + +- : INPUT# ( -- N true | false ) + INPUT$ ( get string ) + NUMBER? ( convert to a string if valid ) + IF DROP TRUE ( get rid of high cell ) + ELSE FALSE + THEN + ; + +This word will return a single-precision number and a TRUE, or it will +just return FALSE. The word **NUMBER?** returns a double precision +number if the input string contains a valid number. Double precision +numbers are 64-bit so we DROP the top 32 bits to get a single-precision +32 bit number. + +## []{#Changing Numeric Base}Changing Numeric Base + +For day-to-day life, the numbering system we use is decimal, or \"base +10.\" That means each digit get multiplied by a power of 10. Thus a +number like 527 is equal to (5\*100 + 2\*10 + 7\*1). The use of 10 for +the numeric base is a completely arbitrary decision. It no doubt has +something to do with the fact that most people have 10 fingers +(including thumbs). The Babylonians used base 60, which is where we got +saddled with the concept of 60 minutes in an hour. Computer hardware +uses base 2, or \"binary\". The computer number \"1101\" is equal to +(1\*8 + 1\*4 + 0\*2 + 1\*1). If you add these up, you get 8+4+1=13 . The +binary number \"10\" is (1\*2 + 0\*1), or 2. Likewise the numeric string +\"10\" in any base N is N. + +Forth makes it very easy to explore different numeric bases because it +can work in any base. Try entering the following: + +- DECIMAL 6 BINARY . + 1 1 + . + 1101 DECIMAL . + +Another useful numeric base is *hexadecimal*. which is base 16. One +problem with bases over 10 is that our normal numbering system only has +digits 0 to 9. For hex numbers we use the letters A to F for the digits +10 to 15. Thus the hex number \"3E7\" is equal to (3\*256 + 14\*16 + +7\*1). Try entering: + +- DECIMAL 12 HEX . \ print C + DECIMAL 12 256 * 7 16 * + 10 + .S + DUP BINARY . + HEX . + +A variable called **BASE** is used to keep track of the current numeric +base. The words **HEX** , **DECIMAL** , and **BINARY** work by changing +this variable. You can change the base to anything you want. Try: + +- 7 BASE ! + 6 1 + . + BASE @ . \ surprise! + +You are now in base 7 . When you fetched and printed the value of BASE, +it said \"10\" because 7, in base 7, is \"10\". + +PForth defines a word called .HEX that prints a number as hexadecimal +regardless of the current base. + +- DECIMAL 14 .HEX + +You could define a word like .HEX for any base. What is needed is a way +to temporarily set the base while a number is printed, then restore it +when we are through. Try the following word: + +- : .BIN ( N -- , print N in Binary ) + BASE @ ( save current base ) + 2 BASE ! ( set to binary ) + SWAP . ( print number ) + BASE ! ( restore base ) + ; + + DECIMAL + 13 .BIN + 13 . + +## []{#Answers to Problems}Answers to Problems + +If your answer doesn\'t exactly match these but it works, don\'t fret. +In Forth, there are usually many ways to the same thing. + +### [Stack Manipulations](#Problems%20-%20Stack) + +- 1) SWAP DUP + 2) ROT DROP + 3) ROT DUP 3 PICK + 4) SWAP OVER 3 PICK + 5) -ROT 2DUP + +### [Arithmetic](#Problems%20-%20Square) + +- (12 * (20 - 17)) ==> 20 17 - 12 * + (1 - (4 * (-18) / 6)) ==> 1 4 -18 * 6 / - + (6 * 13) - (4 * 2 * 7) ==> 6 13 * 4 2 * 7 * - + + : SQUARE ( N -- N*N ) + DUP * + ; + + : DIFF.SQUARES ( A B -- A*A-B*B ) + SWAP SQUARE + SWAP SQUARE - + ; + + : AVERAGE4 ( A B C D -- [A+B+C+D]/4 ) + + + + ( add'em up ) + 4 / + ; + + : HMS>SECONDS ( HOURS MINUTES SECONDS -- TOTAL-SECONDS ) + -ROT SWAP ( -- seconds minutes hours ) + 60 * + ( -- seconds total-minutes ) + 60 * + ( -- seconds ) + ; + +### [Logical Operators](#Problems%20-%20Logical) + +- : LOWERCASE? ( CHAR -- FLAG , true if lowercase ) + DUP 123 < + SWAP 96 > AND + ; + +### [Conditionals](#Problems%20-%20Conditionals) + +- : DEDUCT ( n -- , subtract from account ) + ACCOUNT @ ( -- n acc ) + SWAP - DUP ACCOUNT ! ( -- acc' , update variable ) + ." Balance = $" DUP . CR ( -- acc' ) + 0< ( are we broke? ) + IF ." Warning!! Your account is overdrawn!" CR + THEN + ; + +### [`Loops`](#Problems%20-%20Loops) + +- : SUM.OF.N.1 ( N -- SUM[N] ) + 0 SWAP \ starting value of SUM + 1+ 0 \ set indices for DO LOOP + ?DO \ safer than DO if N=0 + I + + LOOP + ; + + : SUM.OF.N.2 ( N -- SUM[N] ) + 0 \ starting value of SUM + BEGIN ( -- N' SUM ) + OVER + + SWAP 1- SWAP + OVER 0< + UNTIL + SWAP DROP + ; + + : SUM.OF.N.3 ( NUM -- SUM[N] , Gauss' method ) + DUP 1+ \ SUM(N) = N*(N+1)/2 + * 2/ + ; + +Back to [pForth Home Page](../pforth)\ +::: + +::: {#footer} +\(C\) 1997-2015 Mobileer Inc - All Rights Reserved - [Contact +Us](/contacts.php) +::: +::::::::::::: diff --git a/html/XCOM/game.js b/html/XCOM/game.js new file mode 100644 index 0000000..d144401 --- /dev/null +++ b/html/XCOM/game.js @@ -0,0 +1,3598 @@ +// XCOM-like Game - Game Logic and Rendering +// Built using The Elm Architecture pattern + +// ------------------------------------------------ +// TYPE DEFINITIONS +// ------------------------------------------------ + +/** + * @typedef {'floor' | 'wall' | 'obstacle'} TileType + * @typedef {{ type: TileType, x: number, y: number, providesCover: boolean, health: number, damageFlash: number, color: string }} Tile + * @typedef {'player' | 'enemy'} UnitOwner + * @typedef {{ + * id: number, + * owner: UnitOwner, + * x: number, + * y: number, + * maxMovement: number, + * movementRange: number, + * shootRange: number, + * damage: number, + * maxHp: number, + * currentHp: number, + * inCover: boolean, + * hasMoved: boolean, + * hasAttacked: boolean, + * isAnimating: boolean, + * animationType: 'none' | 'moving' | 'shooting' | 'dying', + * animationProgress: number, + * targetX: number, + * targetY: number, + * path: {x: number, y: number}[], + * projectileX: number, + * projectileY: number, + * projectileTargetX: number, + * projectileTargetY: number, + * deathParticles: DeathParticle[], + * isDead: boolean, + * pendingDamage: number, + * justCompletedShot: boolean, + * isVisible: boolean, + * lastSeen: number, + * lastKnownX: number, + * lastKnownY: number, + * aiBehavior: 'aggressive' | 'patrol' | 'stationary', + * turnOrder: number, + * patrolCenterX: number, + * patrolCenterY: number, + * patrolRadius: number, + * actionFeedbackTimer: number + * }} Unit + * @typedef {{ + * x: number, + * y: number, + * velocityX: number, + * velocityY: number, + * size: number, + * life: number, + * maxLife: number, + * color: string + * }} DeathParticle + * @typedef {{ type: 'AwaitingSelection' } | { type: 'UnitSelected', unitId: number } | { type: 'AttackMode', unitId: number }} UIState + * @typedef {{ grid: Tile[][], units: Unit[], selectedUnit: Unit | null, uiState: UIState, currentTurnIndex: number }} Model + */ + +// ------------------------------------------------ +// RENDER CONSTANTS +// ------------------------------------------------ + +const TILE_SIZE = 40; +const UNIT_RADIUS = 15; +const OBSTACLE_MAX_HEALTH = 20; +const MAX_VISIBILITY_RANGE = 6; // Maximum distance units can see +const ENEMY_TURN_SPEED = 0.3; // Enemy turns are 3x faster than player turns +const ENEMY_ACTION_FEEDBACK_DURATION = 800; // How long to show enemy action feedback +const AI_BEHAVIORS = ['aggressive', 'patrol', 'stationary']; +const COLORS = { + gridLine: '#444', + floor: '#2a2a2a', + wall: '#555', + obstacle: '#2C3E50', // Dark blue-grey for obstacles + player: '#00f', + enemy: '#f00', + unitBorder: '#fff', + selectedBorder: '#0f0', + moveHighlight: 'rgba(0, 150, 255, 0.4)', + coverHighlight: 'rgba(255, 215, 0, 0.3)' +}; + +// ------------------------------------------------ +// PROCEDURAL GENERATION CONSTANTS +// ------------------------------------------------ + +const GENERATION_TYPES = { + SPARSE_WAREHOUSE: 'sparse_warehouse', + HALLWAY_ROOMS: 'hallway_rooms', + DRUNKARDS_WALK: 'drunkards_walk', + CELLULAR_AUTOMATA: 'cellular_automata' +}; + +const WAREHOUSE_CONFIG = { + obstacleChance: 0.15, + minObstacles: 3, + maxObstacles: 8 +}; + +const HALLWAY_CONFIG = { + minRoomSize: 3, + maxRoomSize: 6, + minRooms: 2, + maxRooms: 5, + corridorWidth: 2 +}; + +const DRUNKARD_CONFIG = { + steps: 0.4, // Percentage of grid to fill + maxSteps: 1000, + minPathLength: 0.3 // Minimum percentage of floor tiles +}; + +const CELLULAR_CONFIG = { + iterations: 4, + birthThreshold: 4, + survivalThreshold: 3, + minFloorPercentage: 0.4 +}; + +// ------------------------------------------------ +// UTILITY FUNCTIONS +// ------------------------------------------------ + +/** + * Calculates the optimal grid size to fit the viewport + * @returns {{ width: number, height: number }} Grid dimensions + */ +function calculateGridSize() { + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + // Account for button space and borders + const availableWidth = viewportWidth - 40; + const availableHeight = viewportHeight - 40; + + const width = Math.floor(availableWidth / TILE_SIZE); + const height = Math.floor(availableHeight / TILE_SIZE); + + // Ensure minimum grid size + return { + width: Math.max(width, 8), + height: Math.max(height, 6) + }; +} + +/** + * Checks if a path between two points is blocked by obstacles + * @param {number} startX + * @param {number} startY + * @param {number} endX + * @param {number} endY + * @param {Tile[][]} grid + * @returns {boolean} True if path is blocked + */ +function isPathBlocked(startX, startY, endX, endY, grid) { + console.log('isPathBlocked check from', startX, startY, 'to', endX, endY); + + const dx = Math.sign(endX - startX); + const dy = Math.sign(endY - startY); + + let currentX = startX; + let currentY = startY; + + while (currentX !== endX || currentY !== endY) { + if (currentX !== endX) { + currentX += dx; + if (grid[currentY][currentX].type === 'obstacle') { + console.log('Path blocked by obstacle at', currentX, currentY); + return true; + } + } + if (currentY !== endY) { + currentY += dy; + if (grid[currentY][currentX].type === 'obstacle') { + console.log('Path blocked by obstacle at', currentX, currentY); + return true; + } + } + } + + console.log('Path is clear'); + return false; +} + +/** + * Checks if a unit is in cover based on adjacent obstacles + * @param {Unit} unit + * @param {Tile[][]} grid + * @returns {boolean} True if unit is in cover + */ +function checkCover(unit, grid) { + const { x, y } = unit; + const directions = [ + { dx: -1, dy: 0 }, { dx: 1, dy: 0 }, + { dx: 0, dy: -1 }, { dx: 0, dy: 1 } + ]; + + return directions.some(({ dx, dy }) => { + const checkX = x + dx; + const checkY = y + dy; + return checkX >= 0 && checkX < grid[0].length && + checkY >= 0 && checkY < grid.length && + grid[checkY][checkX].type === 'obstacle'; + }); +} + +// ------------------------------------------------ +// PATHFINDING AND ANIMATION +// ------------------------------------------------ + +/** + * A* pathfinding algorithm to find optimal path between two points + * @param {number} startX + * @param {number} startY + * @param {number} endX + * @param {number} endY + * @param {Tile[][]} grid + * @returns {{x: number, y: number}[]} Path array + */ +function findPath(startX, startY, endX, endY, grid) { + const width = grid[0].length; + const height = grid.length; + + // Validate input coordinates + if (startX < 0 || startX >= width || startY < 0 || startY >= height || + endX < 0 || endX >= width || endY < 0 || endY >= height) { + console.error('findPath: Invalid coordinates:', { startX, startY, endX, endY, width, height }); + return []; + } + + // If start and end are the same, no path needed + if (startX === endX && startY === endY) { + return []; + } + + // Simple pathfinding for now - can be enhanced with A* later + const path = []; + let currentX = startX; + let currentY = startY; + + // Move horizontally first, then vertically + while (currentX !== endX) { + const nextX = currentX + Math.sign(endX - currentX); + if (grid[currentY][nextX].type !== 'obstacle') { + currentX = nextX; + path.push({ x: currentX, y: currentY }); + } else { + // Try to go around obstacle + if (currentY + 1 < height && grid[currentY + 1][currentX].type !== 'obstacle') { + currentY++; + path.push({ x: currentX, y: currentY }); + } else if (currentY - 1 >= 0 && grid[currentY - 1][currentX].type !== 'obstacle') { + currentY--; + path.push({ x: currentX, y: currentY }); + } else { + // Can't find path + console.warn('findPath: Cannot find path around obstacle'); + return []; + } + } + } + + while (currentY !== endY) { + const nextY = currentY + Math.sign(endY - currentY); + if (grid[nextY][currentX].type !== 'obstacle') { + currentY = nextY; + path.push({ x: currentX, y: currentY }); + } else { + // Try to go around obstacle + if (currentX + 1 < width && grid[currentY][currentX + 1].type !== 'obstacle') { + currentX++; + path.push({ x: currentX, y: currentY }); + } else if (currentX - 1 >= 0 && grid[currentX - 1][currentY].type !== 'obstacle') { + currentX--; + path.push({ x: currentX, y: currentY }); + } else { + // Can't find path + console.warn('findPath: Cannot find path around obstacle'); + return []; + } + } + } + + // Validate path before returning + const validPath = path.filter(point => + point && typeof point.x === 'number' && typeof point.y === 'number' && + point.x >= 0 && point.x < width && point.y >= 0 && point.y < height + ); + + if (validPath.length !== path.length) { + console.error('findPath: Invalid path points found:', path); + } + + return validPath; +} + +/** + * Starts movement animation for a unit + * @param {Unit} unit + * @param {number} targetX + * @param {number} targetY + * @param {Tile[][]} grid + * @returns {Unit} Updated unit with animation state + */ +function startMovementAnimation(unit, targetX, targetY, grid) { + console.log('startMovementAnimation called with:', { unit: unit.id, from: { x: unit.x, y: unit.y }, to: { x: targetX, y: targetY } }); + + const path = findPath(unit.x, unit.y, targetX, targetY, grid); + console.log('Path found:', path); + + if (path.length === 0) { + console.warn('No path found for unit', unit.id); + return unit; // No path found + } + + const animatedUnit = { + ...unit, + isAnimating: true, + animationType: 'moving', + animationProgress: 0, + targetX, + targetY, + path + }; + + console.log('Animated unit created:', animatedUnit); + return animatedUnit; +} + +/** + * Updates animation progress for all units + * @param {Unit[]} units + * @param {number} deltaTime + * @returns {Unit[]} Updated units + */ +function updateAnimations(units, deltaTime) { + const ANIMATION_SPEED = 0.003; // Adjust for animation speed + + return units.map(unit => { + if (!unit.isAnimating) return unit; + + const newProgress = unit.animationProgress + deltaTime * ANIMATION_SPEED; + + if (newProgress >= 1) { + // Animation complete + if (unit.animationType === 'moving') { + return { + ...unit, + x: unit.targetX, + y: unit.targetY, + hasMoved: true, + isAnimating: false, + animationType: 'none', + animationProgress: 0, + targetX: -1, + targetY: -1, + path: [] + }; + } else if (unit.animationType === 'shooting') { + return { + ...unit, + hasAttacked: true, + isAnimating: false, + animationType: 'none', + animationProgress: 0, + projectileX: -1, + projectileY: -1, + // Don't reset projectileTargetX/Y yet - keep them for damage processing + justCompletedShot: true // Flag to indicate shot just completed + }; + } else if (unit.animationType === 'dying') { + // Death animation is complete, remove the unit + return null; + } + } + + // Update death particles if dying + if (unit.animationType === 'dying') { + const updatedParticles = updateDeathParticles(unit.deathParticles); + if (updatedParticles.length === 0) { + // All particles are gone, complete the death animation + return null; + } + return { + ...unit, + deathParticles: updatedParticles, + animationProgress: newProgress + }; + } + + return { + ...unit, + animationProgress: newProgress + }; + }).filter(unit => unit !== null); // Filter out units that completed their animation +} + +/** + * Gets the current position of a unit during animation + * @param {Unit} unit + * @returns {{x: number, y: number}} Current position + */ +function getUnitPosition(unit) { + // Safety checks + if (!unit) { + console.error('getUnitPosition: unit is undefined'); + return { x: 0, y: 0 }; + } + + if (typeof unit.x !== 'number' || typeof unit.y !== 'number') { + console.error('getUnitPosition: unit coordinates are invalid:', unit); + return { x: 0, y: 0 }; + } + + // If unit is not animating or not moving, return current position + if (!unit.isAnimating || unit.animationType !== 'moving') { + return { x: unit.x, y: unit.y }; + } + + // Safety check for path data + if (!unit.path || !Array.isArray(unit.path) || unit.path.length === 0) { + console.warn('getUnitPosition: unit has invalid path data, returning current position:', unit.id); + return { x: unit.x, y: unit.y }; + } + + // Safety check for animation progress + if (typeof unit.animationProgress !== 'number' || unit.animationProgress < 0 || unit.animationProgress > 1) { + console.warn('getUnitPosition: unit has invalid animation progress, returning current position:', unit.id); + return { x: unit.x, y: unit.y }; + } + + // Calculate position along path + const totalDistance = unit.path.length; + const currentStep = Math.floor(unit.animationProgress * totalDistance); + + if (currentStep >= unit.path.length) { + if (typeof unit.targetX === 'number' && typeof unit.targetY === 'number') { + return { x: unit.targetX, y: unit.targetY }; + } else { + return { x: unit.x, y: unit.y }; + } + } + + const pathPoint = unit.path[currentStep]; + if (pathPoint && typeof pathPoint.x === 'number' && typeof pathPoint.y === 'number') { + return pathPoint; + } else { + console.warn('getUnitPosition: invalid path point at step', currentStep, 'for unit', unit.id, 'path:', unit.path); + return { x: unit.x, y: unit.y }; + } +} + +/** + * Gets the current projectile position during shooting animation + * @param {Unit} unit + * @returns {{x: number, y: number} | null} Current projectile position or null if not shooting + */ +function getProjectilePosition(unit) { + if (!unit.isAnimating || unit.animationType !== 'shooting') { + return null; + } + + if (typeof unit.projectileX !== 'number' || typeof unit.projectileY !== 'number' || + typeof unit.projectileTargetX !== 'number' || typeof unit.projectileTargetY !== 'number') { + return null; + } + + // Calculate projectile position along the line from start to target + const startX = unit.projectileX * TILE_SIZE + TILE_SIZE / 2; + const startY = unit.projectileY * TILE_SIZE + TILE_SIZE / 2; + const targetX = unit.projectileTargetX * TILE_SIZE + TILE_SIZE / 2; + const targetY = unit.projectileTargetY * TILE_SIZE + TILE_SIZE / 2; + + const currentX = startX + (targetX - startX) * unit.animationProgress; + const currentY = startY + (targetY - startY) * unit.animationProgress; + + return { x: currentX, y: currentY }; +} + +/** + * Starts shooting animation for a unit + * @param {Unit} unit + * @param {number} targetX + * @param {number} targetY + * @returns {Unit} Updated unit with shooting animation + */ +function startShootingAnimation(unit, targetX, targetY) { + console.log('Starting shooting animation for unit:', unit.id, 'at target:', targetX, targetY); + return { + ...unit, + isAnimating: true, + animationType: 'shooting', + animationProgress: 0, + projectileX: unit.x, + projectileY: unit.y, + projectileTargetX: targetX, + projectileTargetY: targetY + }; +} + +/** + * Creates death particles for a unit + * @param {Unit} unit + * @returns {Unit} Updated unit with death particles + */ +function createDeathParticles(unit) { + const DEATH_PARTICLE_COUNT = 30; + const DEATH_PARTICLE_SPEED = 8; + const DEATH_PARTICLE_SIZE = 3; + const DEATH_PARTICLE_LIFETIME = 60; + + const centerX = unit.x * TILE_SIZE + TILE_SIZE / 2; + const centerY = unit.y * TILE_SIZE + TILE_SIZE / 2; + + const deathParticles = []; + for (let i = 0; i < DEATH_PARTICLE_COUNT; i++) { + const angle = (Math.PI * 2 * i) / DEATH_PARTICLE_COUNT; + const speed = DEATH_PARTICLE_SPEED * (0.5 + Math.random()); + + // Choose color based on unit type + let color; + if (unit.owner === 'player') { + color = ['#4CAF50', '#45a049', '#2E7D32'][Math.floor(Math.random() * 3)]; // Green variants + } else { + color = ['#F44336', '#D32F2F', '#B71C1C'][Math.floor(Math.random() * 3)]; // Red variants + } + + deathParticles.push({ + x: centerX + (Math.random() - 0.5) * 10, + y: centerY + (Math.random() - 0.5) * 10, + velocityX: Math.cos(angle) * speed, + velocityY: Math.sin(angle) * speed, + size: DEATH_PARTICLE_SIZE + Math.random() * 2, + life: DEATH_PARTICLE_LIFETIME, + maxLife: DEATH_PARTICLE_LIFETIME, + color: color + }); + } + + return { + ...unit, + isAnimating: true, + animationType: 'dying', + animationProgress: 0, + deathParticles, + isDead: true + }; +} + +/** + * Updates death particle animations + * @param {DeathParticle[]} particles + * @returns {DeathParticle[]} Updated particles + */ +function updateDeathParticles(particles) { + return particles.map(particle => { + // Apply gravity + particle.velocityY += 0.3; + + // Update position + particle.x += particle.velocityX; + particle.y += particle.velocityY; + + // Reduce life + particle.life--; + + // Add some randomness to movement + particle.velocityX *= 0.98; + particle.velocityY *= 0.98; + + return particle; + }).filter(particle => particle.life > 0); +} + +// ------------------------------------------------ +// PROCEDURAL GENERATION ALGORITHMS +// ------------------------------------------------ + +/** + * Generates a sparse warehouse-like environment + * @param {number} width + * @param {number} height + * @returns {Tile[][]} Generated grid + */ +function generateSparseWarehouse(width, height) { + const grid = Array.from({ length: height }, (_, y) => + Array.from({ length: width }, (_, x) => ({ + type: 'floor', + x, + y, + providesCover: false, + health: 0, + damageFlash: 0, + color: generateObstacleColor() + })) + ); + + // Add random obstacles + const numObstacles = Math.floor( + Math.random() * (WAREHOUSE_CONFIG.maxObstacles - WAREHOUSE_CONFIG.minObstacles + 1) + + WAREHOUSE_CONFIG.minObstacles + ); + + let obstaclesPlaced = 0; + const maxAttempts = width * height * 2; + let attempts = 0; + + while (obstaclesPlaced < numObstacles && attempts < maxAttempts) { + const x = Math.floor(Math.random() * width); + const y = Math.floor(Math.random() * height); + + // Don't place on edges or if already occupied + if (x > 0 && x < width - 1 && y > 0 && y < height - 1 && + grid[y][x].type === 'floor') { + + // Check if obstacle provides meaningful cover (not isolated) + const hasAdjacentFloor = [ + { dx: -1, dy: 0 }, { dx: 1, dy: 0 }, + { dx: 0, dy: -1 }, { dx: 0, dy: 1 } + ].some(({ dx, dy }) => { + const checkX = x + dx; + const checkY = y + dy; + return checkX >= 0 && checkX < width && + checkY >= 0 && checkY < height && + grid[checkY][checkX].type === 'floor'; + }); + + if (hasAdjacentFloor) { + grid[y][x] = createObstacle(x, y); + obstaclesPlaced++; + } + } + attempts++; + } + + return grid; +} + +/** + * Generates a structured hallway and room layout + * @param {number} width + * @param {number} height + * @returns {Tile[][]} Generated grid + */ +function generateHallwayRooms(width, height) { + const grid = Array.from({ length: height }, (_, y) => + Array.from({ length: width }, (_, x) => ({ + type: 'floor', + x, + y, + providesCover: false, + health: 0, + damageFlash: 0, + color: generateObstacleColor() + })) + ); + + // Generate rooms + const numRooms = Math.floor( + Math.random() * (HALLWAY_CONFIG.maxRooms - HALLWAY_CONFIG.minRooms + 1) + + HALLWAY_CONFIG.minRooms + ); + + const rooms = []; + + for (let i = 0; i < numRooms; i++) { + const roomWidth = Math.floor( + Math.random() * (HALLWAY_CONFIG.maxRoomSize - HALLWAY_CONFIG.minRoomSize + 1) + + HALLWAY_CONFIG.minRoomSize + ); + const roomHeight = Math.floor( + Math.random() * (HALLWAY_CONFIG.maxRoomSize - HALLWAY_CONFIG.minRoomSize + 1) + + HALLWAY_CONFIG.minRoomSize + ); + + // Try to place room + let roomPlaced = false; + let attempts = 0; + const maxAttempts = 50; + + while (!roomPlaced && attempts < maxAttempts) { + const startX = Math.floor(Math.random() * (width - roomWidth - 2)) + 1; + const startY = Math.floor(Math.random() * (height - roomHeight - 2)) + 1; + + // Check if room can fit + let canFit = true; + for (let y = startY - 1; y <= startY + roomHeight; y++) { + for (let x = startX - 1; x <= startX + roomWidth; x++) { + if (y < 0 || y >= height || x < 0 || x >= width) { + canFit = false; + break; + } + if (grid[y][x].type === 'obstacle') { + canFit = false; + break; + } + } + if (!canFit) break; + } + + if (canFit) { + // Place room walls + for (let y = startY; y < startY + roomHeight; y++) { + for (let x = startX; x < startX + roomWidth; x++) { + grid[y][x] = createObstacle(x, y); + } + } + + // Add some interior obstacles for cover + const interiorObstacles = Math.floor(Math.random() * 3) + 1; + for (let j = 0; j < interiorObstacles; j++) { + const obstacleX = startX + 1 + Math.floor(Math.random() * (roomWidth - 2)); + const obstacleY = startY + 1 + Math.floor(Math.random() * (roomHeight - 2)); + + if (grid[obstacleY][obstacleX].type === 'obstacle') { + grid[obstacleY][obstacleX] = { + type: 'floor', + x: obstacleX, + y: obstacleY, + providesCover: false, + health: 0, + damageFlash: 0, + color: generateObstacleColor() + }; + } + } + + rooms.push({ startX, startY, width: roomWidth, height: roomHeight }); + roomPlaced = true; + } + attempts++; + } + } + + // Connect rooms with corridors + if (rooms.length > 1) { + for (let i = 0; i < rooms.length - 1; i++) { + const room1 = rooms[i]; + const room2 = rooms[i + 1]; + + // Create corridor between room centers + const center1X = Math.floor(room1.startX + room1.width / 2); + const center1Y = Math.floor(room1.startY + room1.height / 2); + const center2X = Math.floor(room2.startX + room2.width / 2); + const center2Y = Math.floor(room2.startY + room2.height / 2); + + // Horizontal corridor + const corridorStartX = Math.min(center1X, center2X); + const corridorEndX = Math.max(center1X, center2X); + for (let x = corridorStartX; x <= corridorEndX; x++) { + if (x >= 0 && x < width) { + for (let y = center1Y - 1; y <= center1Y + 1; y++) { + if (y >= 0 && y < height && grid[y][x].type === 'floor') { + grid[y][x] = { + type: 'obstacle', + x, + y, + providesCover: true, + health: OBSTACLE_MAX_HEALTH, // 6 shots to destroy + damageFlash: 0, + color: generateObstacleColor() + }; + } + } + } + } + + // Vertical corridor + const corridorStartY = Math.min(center1Y, center2Y); + const corridorEndY = Math.max(center1Y, center2Y); + for (let y = corridorStartY; y <= corridorEndY; y++) { + if (y >= 0 && y < height) { + for (let x = center2X - 1; x <= center2X + 1; x++) { + if (x >= 0 && x < width && grid[y][x].type === 'floor') { + grid[y][x] = { + type: 'obstacle', + x, + y, + providesCover: true, + health: OBSTACLE_MAX_HEALTH, // 6 shots to destroy + damageFlash: 0, + color: generateObstacleColor() + }; + } + } + } + } + } + } + + return grid; +} + +/** + * Generates a level using the Drunkard's Walk algorithm + * @param {number} width + * @param {number} height + * @returns {Tile[][]} Generated grid + */ +function generateDrunkardsWalk(width, height) { + const grid = Array.from({ length: height }, (_, y) => + Array.from({ length: width }, (_, x) => ({ + type: 'obstacle', + x, + y, + providesCover: true, + health: 0, + damageFlash: 0, + color: generateObstacleColor() + })) + ); + + // Start from center + let currentX = Math.floor(width / 2); + let currentY = Math.floor(height / 2); + + const targetSteps = Math.floor(width * height * DRUNKARD_CONFIG.steps); + let steps = 0; + let maxAttempts = DRUNKARD_CONFIG.maxSteps; + let attempts = 0; + + // Ensure starting position is floor + grid[currentY][currentX] = { + type: 'floor', + x: currentX, + y: currentY, + providesCover: false, + health: 0, + damageFlash: 0, + color: generateObstacleColor() + }; + + while (steps < targetSteps && attempts < maxAttempts) { + // Random direction: 0=up, 1=right, 2=down, 3=left + const direction = Math.floor(Math.random() * 4); + let newX = currentX; + let newY = currentY; + + switch (direction) { + case 0: newY = Math.max(0, currentY - 1); break; // Up + case 1: newX = Math.min(width - 1, currentX + 1); break; // Right + case 2: newY = Math.min(height - 1, currentY + 1); break; // Down + case 3: newX = Math.max(0, currentX - 1); break; // Left + } + + // Carve path + if (grid[newY][newX].type === 'obstacle') { + grid[newY][newX] = { + type: 'floor', + x: newX, + y: newY, + providesCover: false, + health: 0, + damageFlash: 0, + color: generateObstacleColor() + }; + steps++; + } + + currentX = newX; + currentY = newY; + attempts++; + } + + // Add some random floor tiles to ensure connectivity + const additionalFloorTiles = Math.floor(width * height * 0.1); + for (let i = 0; i < additionalFloorTiles; i++) { + const x = Math.floor(Math.random() * width); + const y = Math.floor(Math.random() * height); + + if (grid[y][x].type === 'obstacle') { + // Check if it's adjacent to existing floor + const hasAdjacentFloor = [ + { dx: -1, dy: 0 }, { dx: 1, dy: 0 }, + { dx: 0, dy: -1 }, { dx: 0, dy: 1 } + ].some(({ dx, dy }) => { + const checkX = x + dx; + const checkY = y + dy; + return checkX >= 0 && checkX < width && + checkY >= 0 && checkY < height && + grid[checkY][checkX].type === 'floor'; + }); + + if (hasAdjacentFloor) { + grid[y][x] = { + type: 'floor', + x, + y, + providesCover: false, + health: 0, + damageFlash: 0, + color: generateObstacleColor() + }; + } + } + } + + return grid; +} + +/** + * Generates a level using Cellular Automata + * @param {number} width + * @param {number} height + * @returns {Tile[][]} Generated grid + */ +function generateCellularAutomata(width, height) { + // Initialize with random noise + let grid = Array.from({ length: height }, (_, y) => + Array.from({ length: width }, (_, x) => + Math.random() < 0.45 ? createObstacle(x, y) : { + type: 'floor', + x, + y, + providesCover: false, + health: 0, + damageFlash: 0, + color: generateObstacleColor() + } + ) + ); + + // Ensure edges are obstacles + for (let y = 0; y < height; y++) { + grid[y][0] = createObstacle(0, y); + grid[y][width - 1] = createObstacle(width - 1, y); + } + for (let x = 0; x < width; x++) { + grid[0][x] = createObstacle(x, 0); + grid[height - 1][x] = createObstacle(x, height - 1); + } + + // Run cellular automata iterations + for (let iteration = 0; iteration < CELLULAR_CONFIG.iterations; iteration++) { + const newGrid = Array.from({ length: height }, (_, y) => + Array.from({ length: width }, (_, x) => ({ ...grid[y][x] })) + ); + + for (let y = 1; y < height - 1; y++) { + for (let x = 1; x < width - 1; x++) { + // Count adjacent obstacles + let adjacentObstacles = 0; + for (let dy = -1; dy <= 1; dy++) { + for (let dx = -1; dx <= 1; dx++) { + if (dx === 0 && dy === 0) continue; + if (grid[y + dy][x + dx].type === 'obstacle') { + adjacentObstacles++; + } + } + } + + // Apply rules + if (grid[y][x].type === 'obstacle') { + // Survival rule + if (adjacentObstacles < CELLULAR_CONFIG.survivalThreshold) { + newGrid[y][x].type = 'floor'; + newGrid[y][x].providesCover = false; + } + } else { + // Birth rule + if (adjacentObstacles >= CELLULAR_CONFIG.birthThreshold) { + newGrid[y][x] = createObstacle(x, y); + } + } + } + } + + grid = newGrid; + } + + // Ensure minimum floor percentage + const floorCount = grid.flat().filter(tile => tile.type === 'floor').length; + const minFloorTiles = Math.floor(width * height * CELLULAR_CONFIG.minFloorPercentage); + + if (floorCount < minFloorTiles) { + // Convert some obstacles to floor to meet minimum + const obstaclesToConvert = minFloorTiles - floorCount; + const obstacles = grid.flat().filter(tile => tile.type === 'obstacle' && + tile.x > 0 && tile.x < width - 1 && tile.y > 0 && tile.y < height - 1); + + for (let i = 0; i < Math.min(obstaclesToConvert, obstacles.length); i++) { + const obstacle = obstacles[i]; + grid[obstacle.y][obstacle.x].type = 'floor'; + grid[obstacle.y][obstacle.x].providesCover = false; + } + } + + return grid; +} + +/** + * Checks if a grid is fully navigable using flood fill + * @param {Tile[][]} grid + * @returns {boolean} True if grid is navigable + */ +function isGridNavigable(grid) { + const width = grid[0].length; + const height = grid.length; + + // Find a starting floor tile + let startX = -1, startY = -1; + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + if (grid[y][x].type === 'floor') { + startX = x; + startY = y; + break; + } + } + if (startX !== -1) break; + } + + if (startX === -1) return false; // No floor tiles + + // Flood fill to count accessible floor tiles + const visited = Array.from({ length: height }, () => new Array(width).fill(false)); + const queue = [{ x: startX, y: startY }]; + let accessibleCount = 0; + + while (queue.length > 0) { + const { x, y } = queue.shift(); + + if (visited[y][x] || grid[y][x].type !== 'floor') continue; + + visited[y][x] = true; + accessibleCount++; + + // Add adjacent tiles to queue + const directions = [ + { dx: -1, dy: 0 }, { dx: 1, dy: 0 }, + { dx: 0, dy: -1 }, { dx: 0, dy: 1 } + ]; + + for (const { dx, dy } of directions) { + const newX = x + dx; + const newY = y + dy; + + if (newX >= 0 && newX < width && newY >= 0 && newY < height && + !visited[newY][newX] && grid[newY][newX].type === 'floor') { + queue.push({ x: newX, y: newY }); + } + } + } + + // Count total floor tiles + const totalFloorTiles = grid.flat().filter(tile => tile.type === 'floor').length; + + // Grid is navigable if at least 90% of floor tiles are accessible + return accessibleCount >= totalFloorTiles * 0.9; +} + +/** + * Ensures a grid is navigable by adding connecting paths if needed + * @param {Tile[][]} grid + * @returns {Tile[][]} Navigable grid + */ +function ensureNavigability(grid) { + if (isGridNavigable(grid)) { + return grid; + } + + const width = grid[0].length; + const height = grid.length; + + // Find isolated floor regions and connect them + const visited = Array.from({ length: height }, () => new Array(width).fill(false)); + const regions = []; + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + if (grid[y][x].type === 'floor' && !visited[y][x]) { + // Found new region, flood fill it + const region = []; + const queue = [{ x, y }]; + + while (queue.length > 0) { + const { x: cx, y: cy } = queue.shift(); + + if (visited[cy][cx] || grid[cy][cx].type !== 'floor') continue; + + visited[cy][cx] = true; + region.push({ x: cx, y: cy }); + + const directions = [ + { dx: -1, dy: 0 }, { dx: 1, dy: 0 }, + { dx: 0, dy: -1 }, { dx: 0, dy: 1 } + ]; + + for (const { dx, dy } of directions) { + const newX = cx + dx; + const newY = cy + dy; + + if (newX >= 0 && newX < width && newY >= 0 && newY < height && + !visited[newY][newX] && grid[newY][newX].type === 'floor') { + queue.push({ x: newX, y: newY }); + } + } + } + + if (region.length > 0) { + regions.push(region); + } + } + } + } + + // Connect regions by creating paths between them + for (let i = 0; i < regions.length - 1; i++) { + const region1 = regions[i]; + const region2 = regions[i + 1]; + + // Find closest points between regions + let minDistance = Infinity; + let point1 = null, point2 = null; + + for (const tile1 of region1) { + for (const tile2 of region2) { + const distance = Math.abs(tile1.x - tile2.x) + Math.abs(tile1.y - tile2.y); + if (distance < minDistance) { + minDistance = distance; + point1 = tile1; + point2 = tile2; + } + } + } + + if (point1 && point2) { + // Create path between points + let currentX = point1.x; + let currentY = point1.y; + + while (currentX !== point2.x || currentY !== point2.y) { + if (currentX !== point2.x) { + currentX += Math.sign(point2.x - currentX); + if (grid[currentY][currentX].type === 'obstacle') { + grid[currentY][currentX] = { + type: 'floor', + x: currentX, + y: currentY, + providesCover: false, + health: 0, + damageFlash: 0, + color: generateObstacleColor() + }; + } + } + if (currentY !== point2.y) { + currentY += Math.sign(point2.y - currentY); + if (grid[currentY][currentX].type === 'obstacle') { + grid[currentY][currentX] = { + type: 'floor', + x: currentX, + y: currentY, + providesCover: false, + health: 0, + damageFlash: 0, + color: generateObstacleColor() + }; + } + } + } + } + } + + return grid; +} + +/** + * Generates the game world using a randomly selected algorithm + * @param {number} width + * @param {number} height + * @returns {Tile[][]} Generated grid + */ +function generateWorld(width, height) { + const generationType = Math.random() < 0.25 ? + GENERATION_TYPES.SPARSE_WAREHOUSE : + Math.random() < 0.33 ? GENERATION_TYPES.HALLWAY_ROOMS : + Math.random() < 0.5 ? GENERATION_TYPES.DRUNKARDS_WALK : + GENERATION_TYPES.CELLULAR_AUTOMATA; + + console.log(`Generating ${generationType} layout...`); + + let grid; + switch (generationType) { + case GENERATION_TYPES.SPARSE_WAREHOUSE: + grid = generateSparseWarehouse(width, height); + break; + case GENERATION_TYPES.HALLWAY_ROOMS: + grid = generateHallwayRooms(width, height); + break; + case GENERATION_TYPES.DRUNKARDS_WALK: + grid = generateDrunkardsWalk(width, height); + break; + case GENERATION_TYPES.CELLULAR_AUTOMATA: + grid = generateCellularAutomata(width, height); + break; + default: + grid = generateSparseWarehouse(width, height); + } + + // Ensure navigability + grid = ensureNavigability(grid); + + return grid; +} + +// ------------------------------------------------ +// SQUAD GENERATION +// ------------------------------------------------ + +/** + * Generates a random stat within a given range + * @param {number} min + * @param {number} max + * @returns {number} Random stat value + */ +function generateStat(min, max) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +/** + * Generates a player squad of 3 units + * @param {Tile[][]} grid + * @returns {Unit[]} Array of player units + */ +function generatePlayerSquad(grid) { + const units = []; + const width = grid[0].length; + const height = grid.length; + + // Find a good starting position for the squad + let squadCenterX, squadCenterY; + let attempts = 0; + const maxAttempts = 100; + + do { + squadCenterX = Math.floor(Math.random() * (width - 4)) + 2; + squadCenterY = Math.floor(Math.random() * (height - 4)) + 2; + attempts++; + } while ( + attempts < maxAttempts && + (grid[squadCenterY][squadCenterX].type === 'obstacle' || + grid[squadCenterY][squadCenterX + 1].type === 'obstacle' || + grid[squadCenterY + 1][squadCenterX].type === 'obstacle') + ); + + // Place units in a triangle formation around the center + const positions = [ + { x: squadCenterX, y: squadCenterY }, + { x: squadCenterX + 1, y: squadCenterY }, + { x: squadCenterX, y: squadCenterY + 1 } + ]; + + // Ensure all positions are valid + const validPositions = positions.filter(pos => + pos.x >= 0 && pos.x < width && + pos.y >= 0 && pos.y < height && + grid[pos.y][pos.x].type !== 'obstacle' + ); + + // If we can't place all units in formation, place them randomly but close + if (validPositions.length < 3) { + for (let i = 0; i < 3; i++) { + let x, y; + do { + x = squadCenterX + Math.floor(Math.random() * 3) - 1; + y = squadCenterY + Math.floor(Math.random() * 3) - 1; + x = Math.max(0, Math.min(width - 1, x)); + y = Math.max(0, Math.min(height - 1, y)); + } while (grid[y][x].type === 'obstacle' || + units.some(unit => unit.x === x && unit.y === y)); + + const unit = createPlayerUnit(i + 1, x, y); + units.push(unit); + } + } else { + // Place units in formation + for (let i = 0; i < 3; i++) { + const pos = validPositions[i]; + const unit = createPlayerUnit(i + 1, pos.x, pos.y); + units.push(unit); + } + } + + return units; +} + +/** + * Creates a player unit with the given stats + * @param {number} id + * @param {number} x + * @param {number} y + * @returns {Unit} Player unit + */ +function createPlayerUnit(id, x, y) { + const unit = { + id, + owner: 'player', + x, + y, + maxMovement: generateStat(2, 6), // Reduced from 2-10 to 2-6 + movementRange: 0, // Will be set to maxMovement + shootRange: generateStat(3, 8), // Reduced from 4-15 to 3-8 + damage: generateStat(5, 20), + maxHp: generateStat(30, 60), + currentHp: 0, // Will be set to maxHp + inCover: false, + hasMoved: false, + hasAttacked: false, + isAnimating: false, + animationType: 'none', + animationProgress: 0, + targetX: -1, + targetY: -1, + path: [], + projectileX: -1, + projectileY: -1, + projectileTargetX: -1, + projectileTargetY: -1, + deathParticles: [], + isDead: false, + pendingDamage: 0, + justCompletedShot: false, + isVisible: true, + lastSeen: 0, + lastKnownX: x, + lastKnownY: y, + aiBehavior: 'aggressive', + turnOrder: 0, + patrolCenterX: 0, + patrolCenterY: 0, + patrolRadius: 0, + actionFeedbackTimer: 0 + }; + + // Set current values to max values + unit.movementRange = unit.maxMovement; + unit.currentHp = unit.maxHp; + + // Validate unit creation + console.log('Player unit created:', unit); + + return unit; +} + +/** + * Generates an enemy squad of 2-7 units + * @param {Tile[][]} grid + * @returns {Unit[]} Array of enemy units + */ +function generateEnemySquad(grid) { + const units = []; + const width = grid[0].length; + const height = grid.length; + const squadSize = generateStat(2, 7); + + for (let i = 0; i < squadSize; i++) { + let x, y; + do { + x = Math.floor(Math.random() * width); + y = Math.floor(Math.random() * height); + } while (grid[y][x].type === 'obstacle' || + units.some(unit => unit.x === x && unit.y === y)); + + const unit = createEnemyUnit(100 + i + 1, x, y); + units.push(unit); + } + + return units; +} + +/** + * Creates an enemy unit with the given stats + * @param {number} id + * @param {number} x + * @param {number} y + * @returns {Unit} Enemy unit + */ +function createEnemyUnit(id, x, y) { + const aiBehavior = AI_BEHAVIORS[Math.floor(Math.random() * AI_BEHAVIORS.length)]; + + const unit = { + id, + owner: 'enemy', + x, + y, + maxMovement: generateStat(2, 6), // Reduced from 2-10 to 2-6 + movementRange: 0, // Will be set to maxMovement + shootRange: generateStat(3, 8), // Reduced from 4-15 to 3-8 + damage: generateStat(5, 10), + maxHp: generateStat(10, 20), + currentHp: 0, // Will be set to maxHp + inCover: false, + hasMoved: false, + hasAttacked: false, + isAnimating: false, + animationType: 'none', + animationProgress: 0, + targetX: -1, + targetY: -1, + path: [], + projectileX: -1, + projectileY: -1, + projectileTargetX: -1, + projectileTargetY: -1, + deathParticles: [], + isDead: false, + pendingDamage: 0, + justCompletedShot: false, + isVisible: false, // Enemy units start hidden + lastSeen: 0, + lastKnownX: x, + lastKnownY: y, + aiBehavior, + turnOrder: 0, // Will be set by generateTurnOrder + patrolCenterX: x, // Starting position becomes patrol center + patrolCenterY: y, + patrolRadius: generateStat(3, 8), // Random patrol radius + actionFeedbackTimer: 0 + }; + + // Set current values to max values + unit.movementRange = unit.maxMovement; + unit.currentHp = unit.maxHp; + + // Validate unit creation + console.log('Enemy unit created:', unit); + + return unit; +} + +/** + * Checks for completed shooting animations and applies damage to targets + * @param {Unit[]} units + * @param {Tile[][]} grid + * @returns {{units: Unit[], grid: Tile[][], updated: boolean}} Updated units and grid with damage applied + */ +function processCompletedShots(units, grid) { + let unitsUpdated = false; + let gridUpdated = false; + let newGrid = grid; + + const updatedUnits = units.map(unit => { + // Check if this unit just completed a shooting animation + if (unit.justCompletedShot) { + console.log('Processing completed shot for unit:', unit.id); + console.log('Projectile target coordinates:', unit.projectileTargetX, unit.projectileTargetY); + console.log('All units and their coordinates:'); + units.forEach(u => console.log(`Unit ${u.id} (${u.owner}): x=${u.x}, y=${u.y}`)); + + // Check if we hit an obstacle + if (grid[unit.projectileTargetY] && grid[unit.projectileTargetY][unit.projectileTargetX] && + grid[unit.projectileTargetY][unit.projectileTargetX].type === 'obstacle') { + + console.log('Shot hit obstacle, applying damage'); + if (!gridUpdated) { + newGrid = grid.map(row => [...row]); + gridUpdated = true; + } + + // Get the current obstacle + const currentObstacle = newGrid[unit.projectileTargetY][unit.projectileTargetX]; + + // Apply damage + currentObstacle.health -= 1; + currentObstacle.damageFlash = 1.0; // Set flash effect + + if (currentObstacle.health <= 0) { + // Obstacle destroyed + newGrid[unit.projectileTargetY][unit.projectileTargetX] = { + type: 'floor', + x: unit.projectileTargetX, + y: unit.projectileTargetY, + providesCover: false, + health: 0, + damageFlash: 0, + color: generateObstacleColor() + }; + console.log('Obstacle destroyed by shot!'); + } + + unitsUpdated = true; + } else { + // Find the target unit + const targetUnit = units.find(u => + u.x === unit.projectileTargetX && u.y === unit.projectileTargetY + ); + + console.log('Target unit found:', targetUnit); + console.log('Target pending damage:', targetUnit?.pendingDamage); + + if (targetUnit && targetUnit.pendingDamage > 0) { + // Apply damage to target + const newHp = Math.max(0, targetUnit.currentHp - targetUnit.pendingDamage); + console.log('Applying damage:', targetUnit.pendingDamage, 'New HP:', newHp); + + if (newHp <= 0 && !targetUnit.isDead) { + // Target died - start death animation + console.log('Target died, starting death animation'); + const deadUnit = createDeathParticles({ + ...targetUnit, + currentHp: newHp, + pendingDamage: 0 + }); + + // Update the target unit in the array + const targetIndex = units.findIndex(u => u.id === targetUnit.id); + if (targetIndex !== -1) { + units[targetIndex] = deadUnit; + } + unitsUpdated = true; + } else { + // Target survived - just apply damage + console.log('Target survived with new HP:', newHp); + const updatedTarget = { ...targetUnit, currentHp: newHp, pendingDamage: 0 }; + const targetIndex = units.findIndex(u => u.id === targetUnit.id); + units[targetIndex] = updatedTarget; + unitsUpdated = true; + } + } else { + console.log('No target found or no pending damage'); + } + } + + // Clear the flag and return updated shooting unit + return { + ...unit, + justCompletedShot: false, + projectileTargetX: -1, + projectileTargetY: -1 + }; + } + return unit; + }); + + return { + units: unitsUpdated ? cleanupDeadUnits(units) : updatedUnits, + grid: newGrid, + updated: unitsUpdated || gridUpdated + }; +} + +// ------------------------------------------------ +// 1. STATE INITIALIZATION (MODEL) +// ------------------------------------------------ + +/** + * Creates the initial state of the game. + * @returns {Model} The initial model. + */ +function init() { + const { width, height } = calculateGridSize(); + + // Generate world using procedural generation + const grid = generateWorld(width, height); + + // Generate squads + const playerUnits = generatePlayerSquad(grid); + const enemyUnits = generateEnemySquad(grid); + let units = [...playerUnits, ...enemyUnits]; + + // Generate turn order for all units + const unitsWithTurnOrder = generateTurnOrder(units); + + // Update cover status for all units + unitsWithTurnOrder.forEach(unit => { + unit.inCover = checkCover(unit, grid); + }); + + // Set initial visibility - enemies start hidden unless they're in line of sight of player units + const unitsWithVisibility = updateUnitVisibility(unitsWithTurnOrder, grid); + + // Debug: Show all obstacle health values + console.log('=== GAME INITIALIZATION ==='); + let obstacleCount = 0; + for (let y = 0; y < grid.length; y++) { + for (let x = 0; x < grid[y].length; x++) { + if (grid[y][x].type === 'obstacle') { + obstacleCount++; + } + } + } + console.log(`Total obstacles created: ${obstacleCount}`); + console.log('Initial currentTurnIndex:', 0); + console.log('Initial turn order:', unitsWithVisibility.map(u => ({ id: u.id, owner: u.owner, turnOrder: u.turnOrder }))); + console.log('First unit in turn order:', unitsWithVisibility.find(u => u.turnOrder === 0)); + console.log('=== END INITIALIZATION ==='); + + return { + grid: grid, + units: unitsWithVisibility, + selectedUnit: null, + uiState: { type: 'AwaitingSelection' }, + currentTurnIndex: 0 + }; +} + +// ------------------------------------------------ +// 2. LOGIC (UPDATE) +// ------------------------------------------------ + +/** + * Handles all state changes in the application. + * @param {object} msg - The action message. + * @param {Model} model - The current state. + * @returns {Model} The new state. + */ +function update(msg, model) { + // Check if it's a player unit's turn + const currentUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex); + const isPlayerTurn = currentUnit && currentUnit.owner === 'player'; + + if (!isPlayerTurn) { + return model; + } + + switch (msg.type) { + case 'TILE_CLICKED': { + const { x, y } = msg.payload; + const unitAtTile = model.units.find(u => u.x === x && u.y === y); + + console.log('=== TILE CLICK ==='); + console.log('Clicked at:', x, y); + console.log('Unit at tile:', unitAtTile ? `${unitAtTile.owner} ${unitAtTile.id}` : 'none'); + + // Check if it's a player unit's turn + const currentUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex); + console.log('Current unit in turn order:', currentUnit ? `${currentUnit.owner} ${currentUnit.id}` : 'none'); + console.log('Is player turn:', currentUnit && currentUnit.owner === 'player'); + + if (!currentUnit || currentUnit.owner !== 'player') { + console.log('Not a player unit\'s turn, returning'); + return model; // Not a player unit's turn + } + + if (model.uiState.type === 'UnitSelected') { + const selectedUnit = model.units.find(u => u.id === model.uiState.unitId); + + // Only allow actions if the selected unit is the current unit in turn order + if (selectedUnit.id !== currentUnit.id) { + return model; // Can't act with a different unit + } + + if (!selectedUnit.hasMoved) { + // Movement phase + const distance = Math.abs(selectedUnit.x - x) + Math.abs(selectedUnit.y - y); + const isMoveValid = distance > 0 && + distance <= selectedUnit.movementRange && + !unitAtTile && + model.grid[y][x].type !== 'obstacle' && + !isPathBlocked(selectedUnit.x, selectedUnit.y, x, y, model.grid) && + !selectedUnit.hasMoved; + + if (isMoveValid) { + const newUnits = model.units.map(unit => + unit.id === selectedUnit.id + ? startMovementAnimation(unit, x, y, model.grid) + : unit + ); + + // Update cover status for moved unit + newUnits.forEach(unit => { + unit.inCover = checkCover(unit, model.grid); + }); + + // Update unit visibility after movement + const unitsWithVisibility = updateUnitVisibility(newUnits, model.grid); + + return { ...model, units: unitsWithVisibility, uiState: { type: 'AwaitingSelection' } }; + } + } else if (!selectedUnit.hasAttacked) { + // Attack phase - can attack any unit (including friendly fire) + if (unitAtTile) { + // Can't attack enemy units that are not visible + if (unitAtTile.owner === 'enemy' && !unitAtTile.isVisible) { + return model; // Invalid attack - enemy not visible + } + + // Can't attack units that are at their last known position (ghosts) + if (unitAtTile.owner === 'enemy' && + unitAtTile.lastSeen && + (unitAtTile.x !== unitAtTile.lastKnownX || unitAtTile.y !== unitAtTile.lastKnownY)) { + return model; // Invalid attack - attacking ghost position + } + + const distance = Math.abs(selectedUnit.x - unitAtTile.x) + Math.abs(selectedUnit.y - unitAtTile.y); + const isAttackValid = distance <= selectedUnit.shootRange && !selectedUnit.hasAttacked; + + if (isAttackValid) { + // Check line of sight + const los = checkLineOfSight( + selectedUnit.x, selectedUnit.y, + unitAtTile.x, unitAtTile.y, + model.grid, model.units + ); + + if (los.blocked) { + // Line of sight is blocked - handle stray shot + let damage = selectedUnit.damage; + if (los.obstacleX !== null && los.obstacleY !== null) { + // Hit an obstacle + console.log('Stray shot hit obstacle at:', los.obstacleX, los.obstacleY); + const newGrid = model.grid.map(row => [...row]); + + // Get the current obstacle + const currentObstacle = newGrid[los.obstacleY][los.obstacleX]; + + // Apply damage + currentObstacle.health -= 1; + currentObstacle.damageFlash = 1.0; // Set flash effect + + if (currentObstacle.health <= 0) { + // Obstacle destroyed + newGrid[los.obstacleY][los.obstacleX] = { + type: 'floor', + x: los.obstacleX, + y: los.obstacleY, + providesCover: false, + health: 0, + damageFlash: 0, + color: generateObstacleColor() + }; + console.log('Obstacle destroyed by stray shot!'); + } + + // Start shooting animation towards the obstacle + const newUnits = model.units.map(unit => + unit.id === selectedUnit.id + ? startShootingAnimation(unit, los.obstacleX, los.obstacleY) + : unit + ); + + return { + ...model, + grid: newGrid, + units: newUnits, + uiState: { type: 'AwaitingSelection' } + }; + } else if (los.blocker) { + // Hit a blocking unit + console.log('Stray shot hit blocking unit:', los.blocker.id); + const newUnits = model.units.map(unit => + unit.id === los.blocker.id + ? { ...unit, pendingDamage: damage } + : unit.id === selectedUnit.id + ? startShootingAnimation(unit, los.blocker.x, los.blocker.y) + : unit + ); + + return { ...model, units: newUnits, uiState: { type: 'AwaitingSelection' } }; + } + } else { + // Clear line of sight - proceed with normal attack + // Calculate damage (cover reduces damage by 50%) + let damage = selectedUnit.damage; + if (unitAtTile.inCover) { + damage = Math.floor(damage * 0.5); + } + + // Start shooting animation and mark target for damage + const newUnits = model.units.map(unit => + unit.id === unitAtTile.id + ? { ...unit, pendingDamage: damage } // Mark target for damage when projectile hits + : unit.id === selectedUnit.id + ? startShootingAnimation(unit, unitAtTile.x, unitAtTile.y) + : unit + ); + + return { ...model, units: newUnits, uiState: { type: 'AwaitingSelection' } }; + } + } + } + } + } + + // Check if we can target an obstacle directly + if (model.uiState.type === 'UnitSelected' && model.grid[y][x].type === 'obstacle') { + const selectedUnit = model.units.find(u => u.id === model.uiState.unitId); + + if (selectedUnit && !selectedUnit.hasAttacked) { + const distance = Math.abs(selectedUnit.x - x) + Math.abs(selectedUnit.y - y); + const isAttackValid = distance <= selectedUnit.shootRange && !selectedUnit.hasAttacked; + + if (isAttackValid) { + console.log('Direct obstacle attack at:', x, y); + + // Start shooting animation towards the obstacle + const newUnits = model.units.map(unit => + unit.id === selectedUnit.id + ? startShootingAnimation(unit, x, y) + : unit + ); + + return { ...model, units: newUnits, uiState: { type: 'AwaitingSelection' } }; + } + } + } + + if (unitAtTile && unitAtTile.owner === 'player' && + (!unitAtTile.hasMoved || !unitAtTile.hasAttacked)) { + + console.log('Attempting to select player unit:', unitAtTile.id); + console.log('Unit state:', { + hasMoved: unitAtTile.hasMoved, + hasAttacked: unitAtTile.hasAttacked + }); + + // Only allow selecting the current unit in turn order + if (unitAtTile.id !== currentUnit.id) { + console.log('Cannot select unit - not current unit in turn order'); + return model; // Can't select a different unit + } + + console.log('Successfully selecting unit:', unitAtTile.id); + return { ...model, uiState: { type: 'UnitSelected', unitId: unitAtTile.id } }; + } + + // Can't select enemy units that are not visible + if (unitAtTile && unitAtTile.owner === 'enemy' && !unitAtTile.isVisible) { + return model; // Invalid selection - enemy not visible + } + + return { ...model, uiState: { type: 'AwaitingSelection' } }; + } + case 'SKIP_MOVEMENT': { + if (model.uiState.type === 'UnitSelected') { + const selectedUnit = model.units.find(u => u.id === model.uiState.unitId); + const currentUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex); + + // Only allow skipping if it's the current unit's turn + if (!currentUnit || selectedUnit.id !== currentUnit.id) { + return model; + } + + const newUnits = model.units.map(unit => + unit.id === selectedUnit.id + ? { ...unit, hasMoved: true } + : unit + ); + return { ...model, units: newUnits, uiState: { type: 'AwaitingSelection' } }; + } + return model; + } + case 'SKIP_ATTACK': { + if (model.uiState.type === 'UnitSelected') { + const selectedUnit = model.units.find(u => u.id === model.uiState.unitId); + const currentUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex); + + // Only allow skipping if it's the current unit's turn + if (!currentUnit || selectedUnit.id !== currentUnit.id) { + return model; + } + + const newUnits = model.units.map(unit => + unit.id === selectedUnit.id + ? { ...unit, hasAttacked: true } + : unit + ); + return { ...model, units: newUnits, uiState: { type: 'AwaitingSelection' } }; + } + return model; + } + case 'END_TURN_CLICKED': { + // Advance to next turn in turn order + return advanceTurn(model); + } + default: + return model; + } +} + +// ------------------------------------------------ +// 3. RENDERER (VIEW) +// ------------------------------------------------ + +/** + * Draws the grid tiles and obstacles. + * @param {Model} model + * @param {CanvasRenderingContext2D} ctx + */ +function drawGrid(model, ctx) { + const gridWidth = model.grid[0].length; + const gridHeight = model.grid.length; + + // Draw tiles + for (let y = 0; y < gridHeight; y++) { + for (let x = 0; x < gridWidth; x++) { + const tile = model.grid[y][x]; + const tileX = x * TILE_SIZE; + const tileY = y * TILE_SIZE; + + // Fill tile + ctx.fillStyle = tile.type === 'obstacle' ? tile.color : COLORS.floor; + ctx.fillRect(tileX, tileY, TILE_SIZE, TILE_SIZE); + + // Draw obstacle health indicator + if (tile.type === 'obstacle' && tile.health < OBSTACLE_MAX_HEALTH) { + // Create a more gradual color fade based on health using grey/black/blue palette + const healthRatio = tile.health / OBSTACLE_MAX_HEALTH; + let damageColor; + + if (healthRatio > 0.83) { + // 5 HP - slight blue tint + damageColor = `rgba(52, 73, 94, ${0.2 + (1 - healthRatio) * 0.3})`; + } else if (healthRatio > 0.66) { + // 4 HP - more blue-grey + damageColor = `rgba(44, 62, 80, ${0.3 + (1 - healthRatio) * 0.4})`; + } else if (healthRatio > 0.5) { + // 3 HP - darker blue-grey + damageColor = `rgba(36, 51, 66, ${0.4 + (1 - healthRatio) * 0.4})`; + } else if (healthRatio > 0.33) { + // 2 HP - very dark blue-grey + damageColor = `rgba(28, 40, 52, ${0.5 + (1 - healthRatio) * 0.4})`; + } else { + // 1 HP - almost black with blue tint + damageColor = `rgba(20, 29, 38, ${0.6 + (1 - healthRatio) * 0.4})`; + } + + ctx.fillStyle = damageColor; + ctx.fillRect(tileX + 2, tileY + 2, TILE_SIZE - 4, TILE_SIZE - 4); + } + + // Draw damage flash effect if obstacle was recently hit + if (tile.type === 'obstacle' && tile.damageFlash && tile.damageFlash > 0) { + ctx.fillStyle = `rgba(52, 152, 219, ${tile.damageFlash * 0.6})`; // Blue-white flash + ctx.fillRect(tileX, tileY, TILE_SIZE, TILE_SIZE); + } + + // Draw grid lines + ctx.strokeStyle = COLORS.gridLine; + ctx.lineWidth = 1; + ctx.strokeRect(tileX, tileY, TILE_SIZE, TILE_SIZE); + } + } +} + +/** + * Draws highlights for valid moves and attacks. + * @param {Model} model + * @param {CanvasRenderingContext2D} ctx + */ +function drawHighlights(model, ctx) { + if (model.uiState.type === 'AwaitingSelection') return; + + const selectedUnit = model.units.find(u => u.id === model.uiState.unitId); + const currentUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex); + + if (!selectedUnit || !currentUnit) return; + + // Only show highlights if the selected unit is the current unit in turn order + if (selectedUnit.id !== currentUnit.id) return; + + if (model.uiState.type === 'UnitSelected' && !selectedUnit.hasMoved) { + // Show movement range + ctx.fillStyle = COLORS.moveHighlight; + for (const row of model.grid) { + for (const tile of row) { + if (tile.type === 'obstacle') continue; + + const distance = Math.abs(selectedUnit.x - tile.x) + Math.abs(selectedUnit.y - tile.y); + if (distance > 0 && distance <= selectedUnit.movementRange) { + // Check if path is not blocked + if (!isPathBlocked(selectedUnit.x, selectedUnit.y, tile.x, tile.y, model.grid)) { + ctx.fillRect(tile.x * TILE_SIZE, tile.y * TILE_SIZE, TILE_SIZE, TILE_SIZE); + } + } + } + } + } + + if (model.uiState.type === 'UnitSelected' && selectedUnit.hasMoved && !selectedUnit.hasAttacked) { + // Show attack range + ctx.fillStyle = 'rgba(255, 0, 0, 0.3)'; + for (const row of model.grid) { + for (const tile of row) { + const distance = Math.abs(selectedUnit.x - tile.x) + Math.abs(selectedUnit.y - tile.y); + if (distance > 0 && distance <= selectedUnit.shootRange) { + ctx.fillRect(tile.x * TILE_SIZE, tile.y * TILE_SIZE, TILE_SIZE, TILE_SIZE); + } + } + } + + // Highlight potential targets + ctx.fillStyle = 'rgba(255, 0, 0, 0.6)'; + model.units.forEach(unit => { + if (unit.id !== selectedUnit.id && + (unit.owner === 'player' || (unit.owner === 'enemy' && unit.isVisible && + unit.x === unit.lastKnownX && unit.y === unit.lastKnownY))) { + const distance = Math.abs(selectedUnit.x - unit.x) + Math.abs(selectedUnit.y - unit.y); + if (distance <= selectedUnit.shootRange) { + ctx.beginPath(); + ctx.arc(unit.x * TILE_SIZE + TILE_SIZE / 2, unit.y * TILE_SIZE + TILE_SIZE / 2, UNIT_RADIUS + 5, 0, Math.PI * 2); + ctx.fill(); + } + } + }); + + // Highlight targetable obstacles + ctx.fillStyle = 'rgba(52, 152, 219, 0.4)'; // Blue highlight for targetable obstacles + for (const row of model.grid) { + for (const tile of row) { + if (tile.type === 'obstacle') { + const distance = Math.abs(selectedUnit.x - tile.x) + Math.abs(selectedUnit.y - tile.y); + if (distance > 0 && distance <= selectedUnit.shootRange) { + ctx.fillRect(tile.x * TILE_SIZE + 2, tile.y * TILE_SIZE + 2, TILE_SIZE - 4, TILE_SIZE - 4); + } + } + } + } + } +} + +/** + * Draws the units on the grid. + * @param {Model} model + * @param {CanvasRenderingContext2D} ctx + */ +function drawUnits(model, ctx) { + model.units.forEach(unit => { + if (unit.isDead) return; + + // Sanitize unit state to prevent crashes + const sanitizedUnit = sanitizeUnitState(unit); + + let drawPosition; + if (sanitizedUnit.owner === 'enemy' && !sanitizedUnit.isVisible && sanitizedUnit.lastSeen) { + // Draw at last known position for invisible enemy units + drawPosition = { x: sanitizedUnit.lastKnownX, y: sanitizedUnit.lastKnownY }; + } else { + // Draw at current position for visible units + try { + drawPosition = getUnitPosition(sanitizedUnit); + } catch (error) { + console.warn('Error getting position for unit', sanitizedUnit.id, ':', error); + drawPosition = { x: sanitizedUnit.x, y: sanitizedUnit.y }; + } + } + + if (!drawPosition || typeof drawPosition.x !== 'number' || typeof drawPosition.y !== 'number') { + console.warn('Invalid position for unit:', sanitizedUnit.id, drawPosition, 'skipping render'); + return; // Skip this unit + } + + const { x, y } = drawPosition; + const centerX = x * TILE_SIZE + TILE_SIZE / 2; + const centerY = y * TILE_SIZE + TILE_SIZE / 2; + + // For invisible enemy units, draw as a ghost + if (sanitizedUnit.owner === 'enemy' && !sanitizedUnit.isVisible) { + ctx.globalAlpha = 0.3; // Make them semi-transparent + ctx.fillStyle = '#666'; // Grey color for ghosts + } else { + ctx.globalAlpha = 1.0; // Full opacity for visible units + ctx.fillStyle = sanitizedUnit.owner === 'player' ? COLORS.player : COLORS.enemy; + } + + // Draw cover indicator + if (sanitizedUnit.inCover) { + ctx.fillStyle = COLORS.coverHighlight; + ctx.beginPath(); + ctx.arc(centerX, centerY, UNIT_RADIUS + 5, 0, Math.PI * 2); + ctx.fill(); + } + + // Draw shooting animation effect + if (sanitizedUnit.isAnimating && sanitizedUnit.animationType === 'shooting') { + const pulseSize = UNIT_RADIUS + 10 + Math.sin(sanitizedUnit.animationProgress * Math.PI * 4) * 5; + ctx.fillStyle = 'rgba(255, 255, 0, 0.3)'; + ctx.beginPath(); + ctx.arc(centerX, centerY, pulseSize, 0, Math.PI * 2); + ctx.fill(); + + // Draw projectile + const projectilePos = getProjectilePosition(sanitizedUnit); + if (projectilePos) { + // Draw projectile trail + const trailLength = 15; + const angle = Math.atan2(projectilePos.y - centerY, projectilePos.x - centerX); + const trailStartX = projectilePos.x - Math.cos(angle) * trailLength; + const trailStartY = projectilePos.y - Math.sin(angle) * trailLength; + + // Gradient for trail effect + const gradient = ctx.createLinearGradient(trailStartX, trailStartY, projectilePos.x, projectilePos.y); + gradient.addColorStop(0, 'rgba(255, 255, 0, 0)'); + gradient.addColorStop(0.5, 'rgba(255, 255, 0, 0.8)'); + gradient.addColorStop(1, 'rgba(255, 255, 0, 1)'); + + ctx.strokeStyle = gradient; + ctx.lineWidth = 3; + ctx.lineCap = 'round'; + ctx.beginPath(); + ctx.moveTo(trailStartX, trailStartY); + ctx.lineTo(projectilePos.x, projectilePos.y); + ctx.stroke(); + + // Draw projectile bolt + ctx.fillStyle = '#FFFF00'; + ctx.beginPath(); + ctx.arc(projectilePos.x, projectilePos.y, 4, 0, Math.PI * 2); + ctx.fill(); + + // Add glow effect + ctx.shadowColor = '#FFFF00'; + ctx.shadowBlur = 8; + ctx.beginPath(); + ctx.arc(projectilePos.x, projectilePos.y, 2, 0, Math.PI * 2); + ctx.fill(); + ctx.shadowBlur = 0; + + // Draw impact effect when projectile reaches target + if (sanitizedUnit.animationProgress > 0.8) { + const targetCenterX = sanitizedUnit.projectileTargetX * TILE_SIZE + TILE_SIZE / 2; + const targetCenterY = sanitizedUnit.projectileTargetY * TILE_SIZE + TILE_SIZE / 2; + + // Impact explosion + const explosionSize = (sanitizedUnit.animationProgress - 0.8) * 20; + const alpha = 1 - (sanitizedUnit.animationProgress - 0.8) * 5; + + ctx.fillStyle = `rgba(255, 100, 0, ${alpha})`; + ctx.beginPath(); + ctx.arc(targetCenterX, targetCenterY, explosionSize, 0, Math.PI * 2); + ctx.fill(); + + // Inner explosion + ctx.fillStyle = `rgba(255, 255, 0, ${alpha * 0.8})`; + ctx.beginPath(); + ctx.arc(targetCenterX, targetCenterY, explosionSize * 0.6, 0, Math.PI * 2); + ctx.fill(); + } + } + } + + // Draw death particles + if (sanitizedUnit.isAnimating && sanitizedUnit.animationType === 'dying' && sanitizedUnit.deathParticles) { + sanitizedUnit.deathParticles.forEach(particle => { + const alpha = particle.life / particle.maxLife; + const size = particle.size * alpha; + + ctx.fillStyle = particle.color; + ctx.globalAlpha = alpha; + ctx.beginPath(); + ctx.arc(particle.x, particle.y, size, 0, Math.PI * 2); + ctx.fill(); + + // Add glow effect + ctx.shadowColor = particle.color; + ctx.shadowBlur = size * 2; + ctx.beginPath(); + ctx.arc(particle.x, particle.y, size * 0.5, 0, Math.PI * 2); + ctx.fill(); + ctx.shadowBlur = 0; + }); + ctx.globalAlpha = 1; // Reset global alpha + } + + // Draw targeting reticle for units in attack range + if (model.uiState.type === 'UnitSelected' && + model.uiState.unitId !== sanitizedUnit.id && + sanitizedUnit.owner !== 'player') { + const selectedUnit = model.units.find(u => u.id === model.uiState.unitId); + if (selectedUnit && selectedUnit.hasMoved && !selectedUnit.hasAttacked) { + const distance = Math.abs(selectedUnit.x - sanitizedUnit.x) + Math.abs(selectedUnit.y - sanitizedUnit.y); + if (distance <= selectedUnit.shootRange) { + // Draw targeting reticle + ctx.strokeStyle = '#FF0000'; + ctx.lineWidth = 2; + ctx.setLineDash([5, 5]); + + const reticleSize = UNIT_RADIUS + 8; + ctx.beginPath(); + ctx.arc(centerX, centerY, reticleSize, 0, Math.PI * 2); + ctx.stroke(); + + // Draw crosshairs + ctx.beginPath(); + ctx.moveTo(centerX - reticleSize - 5, centerY); + ctx.lineTo(centerX - reticleSize + 5, centerY); + ctx.moveTo(centerX + reticleSize - 5, centerY); + ctx.lineTo(centerX + reticleSize + 5, centerY); + ctx.moveTo(centerX, centerY - reticleSize - 5); + ctx.lineTo(centerX, centerY - reticleSize + 5); + ctx.moveTo(centerX, centerY + reticleSize - 5); + ctx.lineTo(centerX, centerY + reticleSize + 5); + ctx.stroke(); + + ctx.setLineDash([]); + } + } + } + + // Draw unit + ctx.fillStyle = sanitizedUnit.owner === 'player' ? COLORS.player : COLORS.enemy; + ctx.strokeStyle = (model.uiState.type === 'UnitSelected' && model.uiState.unitId === sanitizedUnit.id) + ? COLORS.selectedBorder + : COLORS.unitBorder; + ctx.lineWidth = 2; + + // Add pending damage indicator + if (sanitizedUnit.pendingDamage > 0) { + ctx.strokeStyle = '#FF0000'; + ctx.lineWidth = 4; + } + + ctx.beginPath(); + ctx.arc(centerX, centerY, UNIT_RADIUS, 0, Math.PI * 2); + ctx.fill(); + ctx.stroke(); + + // Draw HP bar + const hpBarWidth = UNIT_RADIUS * 2; + const hpBarHeight = 4; + const hpBarX = centerX - hpBarWidth / 2; + const hpBarY = centerY - UNIT_RADIUS - 10; + + // Background + ctx.fillStyle = '#333'; + ctx.fillRect(hpBarX, hpBarY, hpBarWidth, hpBarHeight); + + // HP bar + const hpPercentage = sanitizedUnit.currentHp / sanitizedUnit.maxHp; + ctx.fillStyle = hpPercentage > 0.5 ? '#4CAF50' : hpPercentage > 0.25 ? '#FF9800' : '#F44336'; + ctx.fillRect(hpBarX, hpBarY, hpBarWidth * hpPercentage, hpBarHeight); + + // HP text + ctx.fillStyle = '#fff'; + ctx.font = '10px Arial'; + ctx.textAlign = 'center'; + ctx.fillText(`${sanitizedUnit.currentHp}/${sanitizedUnit.maxHp}`, centerX, hpBarY - 2); + + // Draw action indicators + if (sanitizedUnit.owner === 'player') { + // Movement indicator + if (!sanitizedUnit.hasMoved) { + // ctx.fillStyle = '#4CAF50'; + // ctx.beginPath(); + // ctx.arc(centerX - 8, centerY + UNIT_RADIUS + 5, 3, 0, Math.PI * 2); + // ctx.fill(); + } + + // Attack indicator + if (!sanitizedUnit.hasAttacked) { + ctx.fillStyle = sanitizedUnit.hasMoved ? '#FF0000' : '#FF9800'; // Red if ready to attack, orange if waiting + ctx.beginPath(); + ctx.arc(centerX + 8, centerY + UNIT_RADIUS + 5, 3, 0, Math.PI * 2); + ctx.fill(); + } + + // Phase indicator text + if (model.uiState.type === 'UnitSelected' && model.uiState.unitId === sanitizedUnit.id) { + ctx.fillStyle = '#fff'; + ctx.font = '12px Arial'; + ctx.textAlign = 'center'; + if (!sanitizedUnit.hasMoved) { + // ctx.fillText('MOVE', centerX, centerY - UNIT_RADIUS - 15); + } else if (!sanitizedUnit.hasAttacked) { + // ctx.fillText('ATTACK', centerX, centerY - UNIT_RADIUS - 15); + } + } + } + + // Draw turn indicator for current unit + const currentUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex); + if (currentUnit && currentUnit.id === sanitizedUnit.id) { + // Draw turn indicator ring + ctx.strokeStyle = sanitizedUnit.owner === 'player' ? '#00FF00' : '#FF0000'; + ctx.lineWidth = 3; + ctx.beginPath(); + ctx.arc(centerX, centerY, UNIT_RADIUS + 8, 0, Math.PI * 2); + ctx.stroke(); + + // Draw turn indicator text + ctx.fillStyle = sanitizedUnit.owner === 'player' ? '#00FF00' : '#FF0000'; + ctx.font = '14px Arial'; + ctx.textAlign = 'center'; + // ctx.fillText(sanitizedUnit.owner === 'player' ? 'YOUR TURN' : 'ENEMY TURN', centerX, centerY + UNIT_RADIUS + 25); + + // For player units, add a subtle glow to show they're selectable + if (sanitizedUnit.owner === 'player' && !sanitizedUnit.hasMoved && !sanitizedUnit.hasAttacked) { + ctx.shadowColor = '#00FF00'; + ctx.shadowBlur = 15; + ctx.beginPath(); + ctx.arc(centerX, centerY, UNIT_RADIUS + 2, 0, Math.PI * 2); + ctx.stroke(); + ctx.shadowBlur = 0; + } + + // For enemy units, show action feedback + if (sanitizedUnit.owner === 'enemy' && sanitizedUnit.actionFeedbackTimer > 0) { + // Draw action feedback ring + const feedbackAlpha = sanitizedUnit.actionFeedbackTimer / ENEMY_ACTION_FEEDBACK_DURATION; + ctx.strokeStyle = `rgba(255, 255, 0, ${feedbackAlpha})`; + ctx.lineWidth = 4; + ctx.beginPath(); + ctx.arc(centerX, centerY, UNIT_RADIUS + 15, 0, Math.PI * 2); + ctx.stroke(); + + // Draw action feedback text + ctx.fillStyle = `rgba(255, 255, 0, ${feedbackAlpha})`; + ctx.font = '12px Arial'; + ctx.textAlign = 'center'; + + if (sanitizedUnit.isAnimating) { + if (sanitizedUnit.animationType === 'moving') { + ctx.fillText('MOVING...', centerX, centerY + UNIT_RADIUS + 40); + } else if (sanitizedUnit.animationType === 'shooting') { + ctx.fillText('ATTACKING...', centerX, centerY + UNIT_RADIUS + 40); + } + } else { + ctx.fillText('THINKING...', centerX, centerY + UNIT_RADIUS + 40); + } + } + + // Show thinking indicator for current enemy unit even without action feedback + if (sanitizedUnit.owner === 'enemy' && currentUnit && currentUnit.id === sanitizedUnit.id && + !sanitizedUnit.actionFeedbackTimer && !sanitizedUnit.isAnimating) { + // Draw subtle thinking indicator + ctx.strokeStyle = 'rgba(255, 255, 0, 0.3)'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.arc(centerX, centerY, UNIT_RADIUS + 12, 0, Math.PI * 2); + ctx.stroke(); + + // Draw thinking text + ctx.fillStyle = 'rgba(255, 255, 0, 0.5)'; + ctx.font = '10px Arial'; + ctx.textAlign = 'center'; + ctx.fillText('THINKING...', centerX, centerY + UNIT_RADIUS + 35); + } + } + + // Draw movement path preview + if (sanitizedUnit.isAnimating && sanitizedUnit.animationType === 'moving' && sanitizedUnit.path.length > 0) { + ctx.strokeStyle = 'rgba(0, 255, 0, 0.5)'; + ctx.lineWidth = 2; + ctx.setLineDash([5, 5]); + + ctx.beginPath(); + ctx.moveTo(sanitizedUnit.x * TILE_SIZE + TILE_SIZE / 2, sanitizedUnit.y * TILE_SIZE + TILE_SIZE / 2); + + for (const pathPoint of sanitizedUnit.path) { + ctx.lineTo(pathPoint.x * TILE_SIZE + TILE_SIZE / 2, pathPoint.y * TILE_SIZE + TILE_SIZE / 2); + } + + ctx.stroke(); + ctx.setLineDash([]); + } + }); + + // Reset global alpha to ensure proper rendering + ctx.globalAlpha = 1.0; + } + +/** + * Renders the entire game state to the canvas. + * @param {Model} model - The current state to render. + * @param {HTMLCanvasElement} canvas + * @param {CanvasRenderingContext2D} ctx + */ +function view(model, canvas, ctx) { + // Pre-condition: canvas and context are valid. + ctx.clearRect(0, 0, canvas.width, canvas.height); + + drawGrid(model, ctx); + drawHighlights(model, ctx); + drawUnits(model, ctx); + drawFogOfWar(model, ctx); // Add fog of war effect + drawStatusMessage(model, ctx); // Add status message + drawVisibilityRanges(model, ctx); // Add visibility ranges + // Post-condition: canvas displays the current model state. +} + +/** + * Draws status messages at the top of the screen + * @param {Model} model + * @param {HTMLCanvasElement} ctx + */ +function drawStatusMessage(model, ctx) { + const currentUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex); + if (!currentUnit) return; + + // Draw status message at top of screen + ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; + ctx.fillRect(0, 0, ctx.canvas.width, 40); + + ctx.fillStyle = '#FFFFFF'; + ctx.font = '16px Arial'; + ctx.textAlign = 'center'; + + if (currentUnit.owner === 'enemy') { + if (currentUnit.isAnimating) { + if (currentUnit.animationType === 'moving') { + ctx.fillText(`Enemy ${currentUnit.id} is moving...`, ctx.canvas.width / 2, 25); + } else if (currentUnit.animationType === 'shooting') { + ctx.fillText(`Enemy ${currentUnit.id} is attacking...`, ctx.canvas.width / 2, 25); + } + } else { + ctx.fillText(`Enemy ${currentUnit.id}'s turn`, ctx.canvas.width / 2, 25); + } + } else { + ctx.fillText(`Player ${currentUnit.id}'s turn - Move and Attack`, ctx.canvas.width / 2, 25); + } +} + +/** + * Draws visibility range indicators around player units + * @param {Model} model + * @param {CanvasRenderingContext2D} ctx + */ +function drawVisibilityRanges(model, ctx) { + model.units.forEach(unit => { + if (unit.owner === 'player' && !unit.isDead) { + const centerX = unit.x * TILE_SIZE + TILE_SIZE / 2; + const centerY = unit.y * TILE_SIZE + TILE_SIZE / 2; + + // Draw visibility range circle + ctx.strokeStyle = 'rgba(0, 255, 255, 0.3)'; // Cyan with transparency + ctx.lineWidth = 1; + ctx.setLineDash([3, 3]); // Dashed line + + ctx.beginPath(); + ctx.arc(centerX, centerY, MAX_VISIBILITY_RANGE * TILE_SIZE, 0, Math.PI * 2); + ctx.stroke(); + + ctx.setLineDash([]); // Reset line dash + } + }); +} + +// ------------------------------------------------ +// 4. MAIN APPLICATION LOOP +// ------------------------------------------------ + +/** + * The main driver for the application. + */ +function App() { + const canvas = document.getElementById('gameCanvas'); + const endTurnBtn = document.getElementById('endTurnBtn'); + const skipMovementBtn = document.getElementById('skipMovementBtn'); + const skipAttackBtn = document.getElementById('skipAttackBtn'); + + if (!canvas || !endTurnBtn || !skipMovementBtn || !skipAttackBtn) return; + + const ctx = canvas.getContext('2d'); + let model = init(); + let lastTime = 0; + let animationId; + + // Set canvas dimensions based on model + canvas.width = model.grid[0].length * TILE_SIZE; + canvas.height = model.grid.length * TILE_SIZE; + + /** + * Updates button states based on current game state + */ + function updateButtonStates() { + const currentUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex); + const isPlayerTurn = currentUnit && currentUnit.owner === 'player'; + + // Only enable end turn button if it's a player turn and they've completed their actions + if (isPlayerTurn) { + const hasUnfinishedActions = !currentUnit.hasMoved || !currentUnit.hasAttacked; + const hasAnimatingUnits = model.units.some(u => u.isAnimating); + endTurnBtn.disabled = hasUnfinishedActions || hasAnimatingUnits; + } else { + endTurnBtn.disabled = true; // Disable during enemy turns + } + + // Only enable skip buttons if it's the current unit's turn and they're selected + if (model.uiState.type === 'UnitSelected' && isPlayerTurn) { + const selectedUnit = model.units.find(u => u.id === model.uiState.unitId); + if (selectedUnit && selectedUnit.id === currentUnit.id) { + skipMovementBtn.disabled = selectedUnit.hasMoved || selectedUnit.isAnimating; + skipAttackBtn.disabled = selectedUnit.hasAttacked || selectedUnit.isAnimating; + } else { + skipMovementBtn.disabled = true; + skipAttackBtn.disabled = true; + } + } else { + skipMovementBtn.disabled = true; + skipAttackBtn.disabled = true; + } + } + + /** + * Game loop for animations + */ + function gameLoop(currentTime) { + const deltaTime = currentTime - lastTime; + lastTime = currentTime; + + // Update animations + model.units = updateAnimations(model.units, deltaTime); + const shotResult = processCompletedShots(model.units, model.grid); // Process completed shots + model.units = shotResult.units; + if (shotResult.updated) { + model.grid = shotResult.grid; + } + + // Update obstacle flash effects + model.grid = updateObstacleFlash(model.grid); + + // Update unit visibility based on line of sight - ONLY when player units move or when needed + // model.units = updateUnitVisibility(model.units, model.grid); + + // Check if current unit has completed their turn and auto-advance + const currentTurnUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex); + console.log('=== TURN CHECK ==='); + console.log('currentTurnIndex:', model.currentTurnIndex); + console.log('currentTurnUnit:', currentTurnUnit ? `${currentTurnUnit.owner} ${currentTurnUnit.id}` : 'none'); + console.log('currentTurnUnit state:', currentTurnUnit ? { + hasMoved: currentTurnUnit.hasMoved, + hasAttacked: currentTurnUnit.hasAttacked, + isAnimating: currentTurnUnit.isAnimating + } : 'none'); + + if (currentTurnUnit && currentTurnUnit.owner === 'player' && + currentTurnUnit.hasMoved && currentTurnUnit.hasAttacked) { + // Player unit completed their turn, auto-advance + console.log('Player unit completed turn, advancing...'); + model = advanceTurn(model); + + // Check if next unit is enemy and process their turn + const nextUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex); + if (nextUnit && nextUnit.owner === 'enemy') { + console.log('Next unit is enemy, processing their turn...'); + const result = executeEnemyTurn(model, nextUnit); + model = advanceTurn(result); + + // Continue processing enemy turns until we reach a player unit + while (true) { + const nextEnemyUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex); + if (!nextEnemyUnit || nextEnemyUnit.owner === 'player') { + break; // Player turn or no more units + } + console.log('Processing next enemy turn for unit', nextEnemyUnit.id); + const enemyResult = executeEnemyTurn(model, nextEnemyUnit); + model = advanceTurn(enemyResult); + } + } + } + + console.log('After turn advancement check - currentTurnIndex:', model.currentTurnIndex, 'currentUnit:', currentTurnUnit ? `${currentTurnUnit.owner} ${currentTurnUnit.id}` : 'none'); + console.log('=== END TURN CHECK ==='); + + // Update action feedback timers + model.units = updateActionFeedbackTimers(model.units); + + // Render + view(model, canvas, ctx); + updateButtonStates(); + + // Check if we should continue the game loop + const hasAnimations = model.units.some(u => u.isAnimating); + const loopCurrentUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex); + + console.log('Game loop continuation check:', { + hasAnimations, + currentTurnIndex: model.currentTurnIndex, + currentUnit: loopCurrentUnit ? `${loopCurrentUnit.owner} ${loopCurrentUnit.id}` : 'none', + unitState: loopCurrentUnit ? { + hasMoved: loopCurrentUnit.hasMoved, + hasAttacked: loopCurrentUnit.hasAttacked, + isAnimating: loopCurrentUnit.isAnimating + } : 'none' + }); + + if (hasAnimations) { + console.log('Continuing game loop for animations...'); + requestAnimationFrame(gameLoop); + } else { + // If no animations, render once and stop + console.log('Stopping game loop, rendering once...'); + view(model, canvas, ctx); + updateButtonStates(); + } + } + + /** + * The dispatch function, linking events to the update logic. + * @param {object} msg + */ + function dispatch(msg) { + console.log('=== DISPATCH ==='); + console.log('Message type:', msg.type); + + model = update(msg, model); + + console.log('After update - currentTurnIndex:', model.currentTurnIndex); + console.log('Current unit:', model.units.find(u => u.turnOrder === model.currentTurnIndex)); + console.log('Has animations:', model.units.some(u => u.isAnimating)); + + // Start animation loop if needed + if (model.units.some(u => u.isAnimating)) { + console.log('Starting game loop for animations'); + if (animationId) { + cancelAnimationFrame(animationId); + } + lastTime = performance.now(); + animationId = requestAnimationFrame(gameLoop); + } else { + console.log('No animations, just rendering once'); + // Just render once if no animations + view(model, canvas, ctx); + updateButtonStates(); + } + + // Check for game over + const playerUnits = model.units.filter(u => u.owner === 'player' && !u.isDead); + const enemyUnits = model.units.filter(u => u.owner === 'enemy' && !u.isDead); + + if (playerUnits.length === 0) { + alert('Game Over! Enemy wins!'); + return; + } + if (enemyUnits.length === 0) { + alert('Victory! Player wins!'); + return; + } + + + } + + // Setup Event Listeners + canvas.addEventListener('click', (e) => { + const rect = canvas.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + + const tileX = Math.floor(x / TILE_SIZE); + const tileY = Math.floor(y / TILE_SIZE); + + dispatch({ type: 'TILE_CLICKED', payload: { x: tileX, y: tileY } }); + }); + + endTurnBtn.addEventListener('click', () => { + dispatch({ type: 'END_TURN_CLICKED' }); + }); + + skipMovementBtn.addEventListener('click', () => { + dispatch({ type: 'SKIP_MOVEMENT' }); + }); + + skipAttackBtn.addEventListener('click', () => { + dispatch({ type: 'SKIP_ATTACK' }); + }); + + // Handle window resize + window.addEventListener('resize', () => { + // Recalculate grid size and reinitialize if needed + const newSize = calculateGridSize(); + if (newSize.width !== model.grid[0].length || newSize.height !== model.grid.length) { + model = init(); + canvas.width = model.grid[0].length * TILE_SIZE; + canvas.height = model.grid.length * TILE_SIZE; + view(model, canvas, ctx); + updateButtonStates(); + } + }); + + // Initial render + dispatch({ type: 'INITIAL_RENDER' }); // Dispatch a dummy event to start +} + +// Start the game when DOM is loaded +document.addEventListener('DOMContentLoaded', App); + +/** + * Checks if there's a clear line of sight between two points + * @param {number} startX + * @param {number} startY + * @param {number} endX + * @param {number} endY + * @param {Tile[][]} grid + * @param {Unit[]} units + * @returns {{blocked: boolean, blocker: Unit | null, obstacleX: number | null, obstacleY: number | null}} Line of sight result + */ +function checkLineOfSight(startX, startY, endX, endY, grid, units) { + const dx = endX - startX; + const dy = endY - startY; + const distance = Math.sqrt(dx * dx + dy * dy); + + // Add maximum visibility range - units can't see beyond this distance + const MAX_VISIBILITY_RANGE = 6; // Reduced from unlimited to 6 tiles + + if (distance === 0) return { blocked: false, blocker: null, obstacleX: null, obstacleY: null }; + + // If target is beyond visibility range, it's blocked + if (distance > MAX_VISIBILITY_RANGE) { + return { + blocked: true, + blocker: null, + obstacleX: null, + obstacleY: null, + reason: 'beyond_visibility_range' + }; + } + + // Use Bresenham's line algorithm to check each tile along the path + const steps = Math.max(Math.abs(dx), Math.abs(dy)); + const xStep = dx / steps; + const yStep = dy / steps; + + for (let i = 1; i <= steps; i++) { + const checkX = Math.round(startX + xStep * i); + const checkY = Math.round(startY + yStep * i); + + // Check if we've reached the target + if (checkX === endX && checkY === endY) { + break; + } + + // Check for obstacles + if (grid[checkY] && grid[checkY][checkX] && grid[checkY][checkX].type === 'obstacle') { + return { + blocked: true, + blocker: null, + obstacleX: checkX, + obstacleY: checkY, + reason: 'obstacle' + }; + } + + // Check for units blocking the path + const blockingUnit = units.find(unit => + unit.x === checkX && unit.y === checkY && !unit.isDead + ); + + if (blockingUnit) { + return { + blocked: true, + blocker: blockingUnit, + obstacleX: null, + obstacleY: null, + reason: 'unit' + }; + } + } + + return { blocked: false, blocker: null, obstacleX: null, obstacleY: null, reason: 'clear' }; +} + +/** + * Updates obstacle damage flash effects + * @param {Tile[][]} grid + * @returns {Tile[][]} Updated grid with reduced flash values + */ +function updateObstacleFlash(grid) { + const FLASH_DECAY_RATE = 0.05; // How fast the flash fades + + return grid.map(row => + row.map(tile => { + if (tile.type === 'obstacle' && tile.damageFlash > 0) { + return { + ...tile, + damageFlash: Math.max(0, tile.damageFlash - FLASH_DECAY_RATE) + }; + } + return tile; + }) + ); +} + +/** + * Generates a varied obstacle color within the grey/black/blue palette + * @returns {string} Hex color for the obstacle + */ +function generateObstacleColor() { + const colors = [ + '#2C3E50', // Dark blue-grey (original) + '#34495E', // Slightly lighter blue-grey + '#2E4053', // Medium blue-grey + '#283747', // Darker blue-grey + '#1B2631', // Very dark blue-grey + '#1F2937' // Dark grey with blue tint + ]; + return colors[Math.floor(Math.random() * colors.length)]; +} + +/** + * Creates a standardized obstacle with consistent properties + * @param {number} x + * @param {number} y + * @returns {Tile} Standardized obstacle tile + */ +function createObstacle(x, y) { + return { + type: 'obstacle', + x, + y, + providesCover: true, + health: OBSTACLE_MAX_HEALTH, + damageFlash: 0, + color: generateObstacleColor() + }; +} + +/** + * Updates unit visibility based on line of sight from player units + * @param {Unit[]} units + * @param {Tile[][]} grid + * @returns {Unit[]} Updated units with visibility updated + */ +function updateUnitVisibility(units, grid) { + const playerUnits = units.filter(unit => unit.owner === 'player' && !unit.isDead); + const enemyUnits = units.filter(unit => unit.owner === 'enemy' && !unit.isDead); + const currentTime = Date.now(); + + // Update units with visibility and memory tracking + const updatedUnits = units.map(unit => { + if (unit.owner === 'enemy') { + // Check if this enemy is currently visible to any player unit + let isCurrentlyVisible = false; + let lastSeenTime = unit.lastSeen || 0; + + // Check line of sight from each player unit + for (const playerUnit of playerUnits) { + const los = checkLineOfSight( + playerUnit.x, playerUnit.y, + unit.x, unit.y, + grid, units + ); + + if (!los.blocked) { + isCurrentlyVisible = true; + lastSeenTime = currentTime; + break; + } + } + + return { + ...unit, + isVisible: isCurrentlyVisible, + lastSeen: lastSeenTime, + lastKnownX: isCurrentlyVisible ? unit.x : (unit.lastKnownX || unit.x), + lastKnownY: isCurrentlyVisible ? unit.y : (unit.lastKnownY || unit.y) + }; + } + return unit; + }); + + return updatedUnits; +} + +/** + * Draws fog of war effects for areas where enemy units might be hidden + * @param {Model} model + * @param {CanvasRenderingContext2D} ctx + */ +function drawFogOfWar(model, ctx) { + // Create a subtle fog effect for areas outside player line of sight + ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; + + // Draw fog over the entire grid + ctx.fillRect(0, 0, model.grid[0].length * TILE_SIZE, model.grid.length * TILE_SIZE); + + // Clear fog around player units (line of sight areas) + const playerUnits = model.units.filter(unit => unit.owner === 'player' && !unit.isDead); + + playerUnits.forEach(playerUnit => { + const centerX = playerUnit.x * TILE_SIZE + TILE_SIZE / 2; + const centerY = playerUnit.y * TILE_SIZE + TILE_SIZE / 2; + const sightRadius = Math.max(playerUnit.shootRange * TILE_SIZE, 100); // Minimum sight radius + + // Create radial gradient to clear fog around player units + const gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, sightRadius); + gradient.addColorStop(0, 'rgba(0, 0, 0, 0)'); + gradient.addColorStop(0.7, 'rgba(0, 0, 0, 0)'); + gradient.addColorStop(1, 'rgba(0, 0, 0, 0.1)'); + + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, model.grid[0].length * TILE_SIZE, model.grid.length * TILE_SIZE); + }); +} + +/** + * Generates a random turn order for all units + * @param {Unit[]} units + * @returns {Unit[]} Units with turn order assigned + */ +function generateTurnOrder(units) { + // Separate player and enemy units + const playerUnits = units.filter(u => u.owner === 'player'); + const enemyUnits = units.filter(u => u.owner === 'enemy'); + + // Shuffle each group separately + const shuffledPlayers = [...playerUnits].sort(() => Math.random() - 0.5); + const shuffledEnemies = [...enemyUnits].sort(() => Math.random() - 0.5); + + // Combine: players first, then enemies + const orderedUnits = [...shuffledPlayers, ...shuffledEnemies]; + + // Assign turn order + return orderedUnits.map((unit, index) => ({ + ...unit, + turnOrder: index + })); +} + +/** + * Finds the best cover position for a unit that allows them to attack while being protected + * @param {Unit} unit + * @param {Tile[][]} grid + * @param {Unit[]} allUnits + * @returns {{x: number, y: number} | null} Best cover position or null if none found + */ +function findBestCoverPosition(unit, grid, allUnits) { + const width = grid[0].length; + const height = grid.length; + const maxSearchDistance = 8; // Don't search too far + + let bestPosition = null; + let bestScore = -1; + + // Search in expanding radius around unit + for (let radius = 1; radius <= maxSearchDistance; radius++) { + for (let dx = -radius; dx <= radius; dx++) { + for (let dy = -radius; dy <= radius; dy++) { + if (Math.abs(dx) + Math.abs(dy) !== radius) continue; // Only check perimeter + + const checkX = unit.x + dx; + const checkY = unit.y + dy; + + // Check bounds + if (checkX < 0 || checkX >= width || checkY < 0 || checkY >= height) continue; + + // Check if position is walkable + if (grid[checkY][checkX].type === 'obstacle') continue; + + // Check if position is occupied + if (allUnits.some(u => u.x === checkX && u.y === checkY && !u.isDead)) continue; + + // Check if position provides cover + const hasCover = checkCover({ x: checkX, y: checkY }, grid); + + // Check if position allows attacking any visible enemies + const canAttackFromHere = allUnits.some(target => + target.owner === 'player' && + !target.isDead && + Math.abs(checkX - target.x) + Math.abs(checkY - target.y) <= unit.shootRange + ); + + // Calculate score: prioritize cover + attack capability + let score = 0; + if (hasCover) score += 10; + if (canAttackFromHere) score += 5; + score -= Math.abs(dx) + Math.abs(dy); // Prefer closer positions + + if (score > bestScore) { + bestScore = score; + bestPosition = { x: checkX, y: checkY }; + } + } + } + } + + return bestPosition; +} + +/** + * Makes AI decisions for an enemy unit + * @param {Unit} unit + * @param {Tile[][]} grid + * @param {Unit[]} allUnits + * @returns {{action: 'move' | 'attack' | 'skip', targetX?: number, targetY?: number, targetUnit?: Unit} | null} AI decision + */ +function makeAIDecision(unit, grid, allUnits) { + console.log('makeAIDecision called for unit', unit.id, 'with behavior:', unit.aiBehavior); + console.log('Unit state:', { hasMoved: unit.hasMoved, hasAttacked: unit.hasAttacked, isDead: unit.isDead }); + + if (unit.isDead || unit.hasMoved && unit.hasAttacked) { + console.log('Unit', unit.id, 'cannot act - dead or completed actions'); + return null; // Unit can't act + } + + // Find visible player units + const visiblePlayers = allUnits.filter(target => + target.owner === 'player' && + !target.isDead && + target.isVisible + ); + + console.log('Visible players for unit', unit.id, ':', visiblePlayers.length); + + // If no visible players, behavior depends on AI type + if (visiblePlayers.length === 0) { + console.log('No visible players for unit', unit.id, 'using behavior:', unit.aiBehavior); + switch (unit.aiBehavior) { + case 'aggressive': + return { type: 'skip' }; // Wait for targets + case 'patrol': + return generatePatrolAction(unit, grid, allUnits); + case 'stationary': + return { type: 'skip' }; // Stay put + } + } + + // If we have visible players, behavior depends on AI type + console.log('Visible players found for unit', unit.id, 'using behavior:', unit.aiBehavior); + switch (unit.aiBehavior) { + case 'aggressive': + return generateAggressiveAction(unit, visiblePlayers, grid, allUnits); + case 'patrol': + return generatePatrolAction(unit, grid, allUnits, visiblePlayers); + case 'stationary': + return generateStationaryAction(unit, visiblePlayers, grid, allUnits); + } + + return { type: 'skip' }; +} + +/** + * Generates aggressive AI action - move toward and attack visible players + * @param {Unit} unit + * @param {Unit[]} visiblePlayers + * @param {Tile[][]} grid + * @param {Unit[]} allUnits + * @returns {{action: string, targetX?: number, targetY?: number, targetUnit?: Unit}} + */ +function generateAggressiveAction(unit, visiblePlayers, grid, allUnits) { + console.log('generateAggressiveAction for unit', unit.id, 'with', visiblePlayers.length, 'visible players'); + + // Find closest visible player + const closestPlayer = visiblePlayers.reduce((closest, player) => { + const distance = Math.abs(unit.x - player.x) + Math.abs(unit.y - player.y); + const closestDistance = Math.abs(unit.x - closest.x) + Math.abs(unit.y - closest.y); + return distance < closestDistance ? player : closest; + }); + + const distance = Math.abs(unit.x - closestPlayer.x) + Math.abs(unit.y - closestPlayer.y); + console.log('Distance to closest player for unit', unit.id, ':', distance, 'shoot range:', unit.shootRange); + + // If we can attack and haven't attacked yet, do it! + if (!unit.hasAttacked && distance <= unit.shootRange) { + console.log('Unit', unit.id, 'can attack player at', closestPlayer.x, closestPlayer.y); + return { type: 'attack', targetX: closestPlayer.x, targetY: closestPlayer.y }; + } + + // If we can't attack but can move and haven't moved yet, move closer + if (!unit.hasMoved && distance > unit.shootRange) { + console.log('Unit', unit.id, 'needs to move closer to attack'); + const moveTarget = findMoveTowardTarget(unit, closestPlayer, grid, allUnits); + if (moveTarget) { + console.log('Move target found for unit', unit.id, ':', moveTarget); + return { type: 'move', x: moveTarget.x, y: moveTarget.y }; + } else { + console.log('No move target found for unit', unit.id); + } + } + + // If we've done what we can, skip the turn + console.log('Unit', unit.id, 'skipping turn - no more actions possible'); + return { type: 'skip' }; +} + +/** + * Generates patrol AI action - defend territory, engage if players enter + * @param {Unit} unit + * @param {Tile[][]} grid + * @param {Unit[]} allUnits + * @param {Unit[]} visiblePlayers + * @returns {{action: string, targetX?: number, targetY?: number, targetUnit?: Unit}} + */ +function generatePatrolAction(unit, grid, allUnits, visiblePlayers = []) { + // If players are visible, engage them + if (visiblePlayers.length > 0) { + return generateAggressiveAction(unit, visiblePlayers, grid, allUnits); + } + + // Otherwise, patrol within territory + if (!unit.hasMoved) { + const patrolTarget = findPatrolPosition(unit, grid, allUnits); + if (patrolTarget) { + return { type: 'move', x: patrolTarget.x, y: patrolTarget.y }; + } + } + + return { type: 'skip' }; +} + +/** + * Generates stationary AI action - attack from cover, flee when attacked + * @param {Unit} unit + * @param {Unit[]} visiblePlayers + * @param {Tile[][]} grid + * @param {Unit[]} allUnits + * @returns {{action: string, targetX?: number, targetY?: number, targetUnit?: Unit}} + */ +function generateStationaryAction(unit, visiblePlayers, grid, allUnits) { + // If we can attack, do it + if (!unit.hasAttacked) { + const attackablePlayer = visiblePlayers.find(player => { + const distance = Math.abs(unit.x - player.x) + Math.abs(unit.y - player.y); + return distance <= unit.shootRange; + }); + + if (attackablePlayer) { + return { type: 'attack', targetX: attackablePlayer.x, targetY: attackablePlayer.y }; + } + } + + // If we're not in cover and can move, try to find cover + if (!unit.hasMoved && !unit.inCover) { + const coverPosition = findBestCoverPosition(unit, grid, allUnits); + if (coverPosition) { + return { type: 'move', x: coverPosition.x, y: coverPosition.y }; + } + } + + return { type: 'skip' }; +} + +/** + * Finds a movement target toward a specific target unit + * @param {Unit} unit + * @param {Unit} target + * @param {Tile[][]} grid + * @param {Unit[]} allUnits + * @returns {{x: number, y: number} | null} Movement target or null if none found + */ +function findMoveTowardTarget(unit, target, grid, allUnits) { + console.log('findMoveTowardTarget for unit', unit.id, 'toward target at', target.x, target.y); + console.log('Unit position:', unit.x, unit.y, 'movement range:', unit.movementRange); + + const width = grid[0].length; + const height = grid.length; + const maxSearchDistance = Math.min(unit.movementRange, 8); + + console.log('Searching within distance:', maxSearchDistance); + + let bestPosition = null; + let bestScore = -1; + + // Search for positions that get us closer to target + for (let dx = -maxSearchDistance; dx <= maxSearchDistance; dx++) { + for (let dy = -maxSearchDistance; dy <= maxSearchDistance; dy++) { + if (Math.abs(dx) + Math.abs(dy) > unit.movementRange) continue; + + const checkX = unit.x + dx; + const checkY = unit.y + dy; + + // Check bounds + if (checkX < 0 || checkX >= width || checkY < 0 || checkY >= height) continue; + + // Check if position is walkable + if (grid[checkY][checkX].type === 'obstacle') continue; + + // Check if position is occupied + if (allUnits.some(u => u.x === checkX && u.y === checkY && !u.isDead)) continue; + + // Check if path is not blocked + if (isPathBlocked(unit.x, unit.y, checkX, checkY, grid)) continue; + + // Calculate score: prefer positions closer to target + const currentDistance = Math.abs(unit.x - target.x) + Math.abs(unit.y - target.y); + const newDistance = Math.abs(checkX - target.x) + Math.abs(checkY - target.y); + const distanceImprovement = currentDistance - newDistance; + + let score = distanceImprovement * 10; // Prioritize getting closer + if (grid[checkY][checkX].providesCover) score += 5; // Bonus for cover + + if (score > bestScore) { + bestScore = score; + bestPosition = { x: checkX, y: checkY }; + } + } + } + + console.log('Best position found for unit', unit.id, ':', bestPosition, 'with score:', bestScore); + return bestPosition; +} + +/** + * Finds a patrol position within the unit's territory + * @param {Unit} unit + * @param {Tile[][]} grid + * @param {Unit[]} allUnits + * @returns {{x: number, y: number} | null} Patrol position or null if none found + */ +function findPatrolPosition(unit, grid, allUnits) { + const width = grid[0].length; + const height = grid.length; + const maxSearchDistance = Math.min(unit.movementRange, unit.patrolRadius); + + let bestPosition = null; + let bestScore = -1; + + // Search for positions within patrol radius + for (let dx = -maxSearchDistance; dx <= maxSearchDistance; dx++) { + for (let dy = -maxSearchDistance; dy <= maxSearchDistance; dy++) { + if (Math.abs(dx) + Math.abs(dy) > unit.movementRange) continue; + + const checkX = unit.x + dx; + const checkY = unit.y + dy; + + // Check bounds + if (checkX < 0 || checkX >= width || checkY < 0 || checkY >= height) continue; + + // Check if position is within patrol radius + const distanceFromCenter = Math.abs(checkX - unit.patrolCenterX) + Math.abs(checkY - unit.patrolCenterY); + if (distanceFromCenter > unit.patrolRadius) continue; + + // Check if position is walkable + if (grid[checkY][checkX].type === 'obstacle') continue; + + // Check if position is occupied + if (allUnits.some(u => u.x === checkX && u.y === checkY && !u.isDead)) continue; + + // Check if path is not blocked + if (isPathBlocked(unit.x, unit.y, checkX, checkY, grid)) continue; + + // Calculate score: prefer positions with good cover and visibility + let score = 0; + if (grid[checkY][checkX].providesCover) score += 8; + + // Bonus for positions that allow seeing outside patrol area + const canSeeOutside = checkX === 0 || checkX === width - 1 || checkY === 0 || checkY === height - 1; + if (canSeeOutside) score += 3; + + // Small random factor to avoid predictable patterns + score += Math.random() * 2; + + if (score > bestScore) { + bestScore = score; + bestPosition = { x: checkX, y: checkY }; + } + } + } + + return bestPosition; +} + +/** + * Advances to the next turn in the turn order + * @param {Model} model + * @returns {Model} Updated model with next turn + */ +function advanceTurn(model) { + let nextTurnIndex = model.currentTurnIndex + 1; + + // Find next living unit + while (nextTurnIndex < model.units.length) { + const nextUnit = model.units.find(u => u.turnOrder === nextTurnIndex); + if (nextUnit && !nextUnit.isDead) { + break; + } + nextTurnIndex++; + } + + // If we've gone through all units, start over + if (nextTurnIndex >= model.units.length) { + nextTurnIndex = 0; + // Reset all units' actions + const updatedUnits = model.units.map(unit => ({ + ...unit, + hasMoved: false, + hasAttacked: false + })); + + return { + ...model, + units: updatedUnits, + currentTurnIndex: nextTurnIndex, + uiState: { type: 'AwaitingSelection' } // Reset UI state + }; + } + + // Reset UI state when advancing to next turn + return { + ...model, + currentTurnIndex: nextTurnIndex, + uiState: { type: 'AwaitingSelection' } + }; +} + +/** + * Executes the current unit's turn (player or AI) + * @param {Model} model + * @returns {Model} Updated model after turn execution + */ +function executeCurrentTurn(model) { + const currentUnit = model.units.find(u => u.turnOrder === model.currentTurnIndex); + + if (!currentUnit || currentUnit.isDead) { + console.log('No current unit or unit is dead, advancing turn'); + return advanceTurn(model); + } + + // If it's a player unit, wait for player input + if (currentUnit.owner === 'player') { + console.log('Player turn, waiting for input'); + return model; + } + + // If it's an enemy unit, execute AI turn immediately + console.log('Enemy turn for unit', currentUnit.id, '- executing AI immediately'); + + // Execute AI turn and advance immediately + const result = executeEnemyTurn(model, currentUnit); + console.log('AI turn completed for unit', currentUnit.id, 'advancing turn'); + + // Always advance turn after enemy completes their actions + return advanceTurn(result); +} + + + +/** + * Removes dead units from the turn order and adjusts turn indices + * @param {Unit[]} units + * @returns {Unit[]} Updated units with dead units removed and turn order adjusted + */ +function cleanupDeadUnits(units) { + const livingUnits = units.filter(unit => !unit.isDead); + + // Reassign turn order for remaining units + return livingUnits.map((unit, index) => ({ + ...unit, + turnOrder: index + })); +} + +/** + * Updates action feedback timers for enemy units + * @param {Unit[]} units + * @returns {Unit[]} Updated units + */ +function updateActionFeedbackTimers(units) { + return units.map(unit => { + if (unit.actionFeedbackTimer > 0) { + return { ...unit, actionFeedbackTimer: Math.max(0, unit.actionFeedbackTimer - 16) }; // 16ms per frame at 60fps + } + return unit; + }); +} + + + + + +/** + * Pure function: Executes enemy turn with guaranteed termination + * @param {Model} model + * @param {Unit} enemyUnit + * @returns {Model} Updated model with turn completed + */ +function executeEnemyTurn(model, enemyUnit) { + // Pre-condition: enemyUnit is an enemy unit that needs to act + if (enemyUnit.owner !== 'enemy') { + throw new Error('executeEnemyTurn called with non-enemy unit'); + } + + console.log('Executing enemy turn for unit', enemyUnit.id); + + // Find visible player units + const visiblePlayers = model.units.filter(target => + target.owner === 'player' && + !target.isDead && + target.isVisible + ); + + if (visiblePlayers.length === 0) { + console.log('No visible players for unit', enemyUnit.id, 'skipping turn'); + // Mark both actions as complete and return + const skippedUnit = { + ...enemyUnit, + hasMoved: true, + hasAttacked: true, + isAnimating: false, + animationType: 'none', + animationProgress: 0, + path: [], + targetX: -1, + targetY: -1 + }; + return { + ...model, + units: model.units.map(u => u.id === enemyUnit.id ? skippedUnit : u) + }; + } + + // Find closest visible player + const closestPlayer = visiblePlayers.reduce((closest, player) => { + const distance = Math.abs(enemyUnit.x - player.x) + Math.abs(enemyUnit.y - player.y); + const closestDistance = Math.abs(enemyUnit.x - closest.x) + Math.abs(enemyUnit.y - closest.y); + return distance < closestDistance ? player : closest; + }); + + const distance = Math.abs(enemyUnit.x - closestPlayer.x) + Math.abs(enemyUnit.y - closestPlayer.y); + console.log('Unit', enemyUnit.id, 'distance to player:', distance, 'shoot range:', enemyUnit.shootRange); + + let updatedModel = model; + let updatedUnit = enemyUnit; + + // First action: Move if we can't attack yet + if (!updatedUnit.hasMoved && distance > enemyUnit.shootRange) { + console.log('Unit', enemyUnit.id, 'moving closer to attack'); + const moveTarget = findMoveTowardTarget(updatedUnit, closestPlayer, updatedModel.grid, updatedModel.units); + if (moveTarget) { + const moveResult = executeEnemyMove(updatedModel, updatedUnit, { type: 'move', x: moveTarget.x, y: moveTarget.y }); + updatedModel = moveResult; + updatedUnit = moveResult.units.find(u => u.id === enemyUnit.id); + console.log('Unit', enemyUnit.id, 'moved to', moveTarget.x, moveTarget.y); + } else { + // Can't move, mark as moved + updatedUnit = { ...updatedUnit, hasMoved: true }; + updatedModel = { + ...updatedModel, + units: updatedModel.units.map(u => u.id === enemyUnit.id ? updatedUnit : u) + }; + } + } else if (!updatedUnit.hasMoved) { + // No movement needed, mark as moved + updatedUnit = { ...updatedUnit, hasMoved: true }; + updatedModel = { + ...updatedModel, + units: updatedModel.units.map(u => u.id === enemyUnit.id ? updatedUnit : u) + }; + } + + // Second action: Attack if we can + if (!updatedUnit.hasAttacked && distance <= enemyUnit.shootRange) { + console.log('Unit', enemyUnit.id, 'attacking player at', closestPlayer.x, closestPlayer.y); + const attackResult = executeEnemyAttack(updatedModel, updatedUnit, { type: 'attack', targetX: closestPlayer.x, targetY: closestPlayer.y }); + updatedModel = attackResult; + updatedUnit = attackResult.units.find(u => u.id === enemyUnit.id); + console.log('Unit', enemyUnit.id, 'attacked'); + } else if (!updatedUnit.hasAttacked) { + // Can't attack, mark as attacked + updatedUnit = { ...updatedUnit, hasAttacked: true }; + updatedModel = { + ...updatedModel, + units: updatedModel.units.map(u => u.id === enemyUnit.id ? updatedUnit : u) + }; + } + + // Ensure both actions are marked as complete + if (!updatedUnit.hasMoved) { + updatedUnit = { + ...updatedUnit, + hasMoved: true, + isAnimating: false, + animationType: 'none', + animationProgress: 0, + path: [], + targetX: -1, + targetY: -1 + }; + } + if (!updatedUnit.hasAttacked) { + updatedUnit = { + ...updatedUnit, + hasAttacked: true, + isAnimating: false, + animationType: 'none', + animationProgress: 0, + projectileX: -1, + projectileY: -1, + projectileTargetX: -1, + projectileTargetY: -1 + }; + } + + // Update the model with the final unit state + const finalModel = { + ...updatedModel, + units: updatedModel.units.map(u => u.id === enemyUnit.id ? updatedUnit : u) + }; + + console.log('Enemy turn completed for unit', enemyUnit.id, 'final state:', { + hasMoved: updatedUnit.hasMoved, + hasAttacked: updatedUnit.hasAttacked + }); + + return finalModel; +} + +/** + * Pure function: Executes enemy movement + * @param {Model} model + * @param {Unit} enemyUnit + * @param {Object} decision + * @returns {Model} Updated model + */ +function executeEnemyMove(model, enemyUnit, decision) { + console.log('Executing enemy move for unit', enemyUnit.id, 'to', decision.x, decision.y); + + const path = findPath(enemyUnit.x, enemyUnit.y, decision.x, decision.y, model.grid); + console.log('Path found:', path); + + if (path.length > 1) { + // Start movement animation + const animatedUnit = startMovementAnimation(enemyUnit, decision.x, decision.y, model.grid); + animatedUnit.hasMoved = true; + console.log('Enemy unit', enemyUnit.id, 'started movement animation'); + + return { + ...model, + units: model.units.map(u => u.id === enemyUnit.id ? animatedUnit : u) + }; + } else { + // No movement needed, mark as moved + console.log('Enemy unit', enemyUnit.id, 'no movement needed, marked as moved'); + return { + ...model, + units: model.units.map(u => + u.id === enemyUnit.id ? { ...u, hasMoved: true } : u + ) + }; + } +} + +/** + * Pure function: Executes enemy attack + * @param {Model} model + * @param {Unit} enemyUnit + * @param {Object} decision + * @returns {Model} Updated model + */ +function executeEnemyAttack(model, enemyUnit, decision) { + console.log('Executing enemy attack for unit', enemyUnit.id, 'at target', decision.targetX, decision.targetY); + + const animatedUnit = startShootingAnimation(enemyUnit, decision.targetX, decision.targetY); + animatedUnit.hasAttacked = true; + console.log('Enemy unit', enemyUnit.id, 'started attack animation'); + + return { + ...model, + units: model.units.map(u => u.id === enemyUnit.id ? animatedUnit : u) + }; +} + +/** + * Sanitizes unit animation state to prevent crashes + * @param {Unit} unit + * @returns {Unit} Sanitized unit + */ +function sanitizeUnitState(unit) { + // If unit is not animating, ensure all animation properties are reset + if (!unit.isAnimating) { + return { + ...unit, + animationType: 'none', + animationProgress: 0, + path: [], + targetX: -1, + targetY: -1, + projectileX: -1, + projectileY: -1, + projectileTargetX: -1, + projectileTargetY: -1 + }; + } + + // If unit is animating, validate animation properties + const sanitizedUnit = { ...unit }; + + // Validate path data + if (sanitizedUnit.animationType === 'moving') { + if (!sanitizedUnit.path || !Array.isArray(sanitizedUnit.path)) { + sanitizedUnit.path = []; + } + if (typeof sanitizedUnit.targetX !== 'number' || typeof sanitizedUnit.targetY !== 'number') { + sanitizedUnit.targetX = sanitizedUnit.x; + sanitizedUnit.targetY = sanitizedUnit.y; + } + } + + // Validate projectile data + if (sanitizedUnit.animationType === 'shooting') { + if (typeof sanitizedUnit.projectileX !== 'number' || typeof sanitizedUnit.projectileY !== 'number') { + sanitizedUnit.projectileX = sanitizedUnit.x; + sanitizedUnit.projectileY = sanitizedUnit.y; + } + if (typeof sanitizedUnit.projectileTargetX !== 'number' || typeof sanitizedUnit.projectileTargetY !== 'number') { + sanitizedUnit.projectileTargetX = sanitizedUnit.x; + sanitizedUnit.projectileTargetY = sanitizedUnit.y; + } + } + + // Validate animation progress + if (typeof sanitizedUnit.animationProgress !== 'number' || + sanitizedUnit.animationProgress < 0 || + sanitizedUnit.animationProgress > 1) { + sanitizedUnit.animationProgress = 0; + } + + return sanitizedUnit; +} + + diff --git a/html/XCOM/index.html b/html/XCOM/index.html new file mode 100644 index 0000000..7ba4346 --- /dev/null +++ b/html/XCOM/index.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>YCOM</title> + <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🎯</text></svg>"> + <link rel="stylesheet" href="style.css"> +</head> +<body> + <div id="gameContainer"> + <canvas id="gameCanvas"></canvas> + <div id="actionButtons"> + <button id="endTurnBtn">End Turn</button> + <button id="skipMovementBtn">Skip Movement</button> + <button id="skipAttackBtn">Skip Attack</button> + </div> + </div> + + <script src="game.js"></script> +</body> +</html> \ No newline at end of file diff --git a/html/XCOM/style.css b/html/XCOM/style.css new file mode 100644 index 0000000..771009b --- /dev/null +++ b/html/XCOM/style.css @@ -0,0 +1,104 @@ +/* XCOM-like Game Styles */ +/* Optimized for both desktop and mobile */ + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #333; + color: #fff; + margin: 0; + padding: 0; + box-sizing: border-box; + overflow: hidden; +} + +#gameContainer { + position: relative; + width: 100vw; + height: 100vh; + display: flex; + justify-content: center; + align-items: center; +} + +canvas { + background: #222; + border: 1px solid #555; + max-width: 100vw; + max-height: 100vh; + display: block; +} + +#actionButtons { + position: absolute; + top: 10px; + right: 10px; + z-index: 1000; + display: flex; + flex-direction: column; + gap: 5px; +} + +#endTurnBtn, #skipMovementBtn, #skipAttackBtn { + margin: 0; + padding: 8px 16px; + font-size: 14px; + cursor: pointer; + background: #4CAF50; + color: white; + border: none; + border-radius: 4px; + transition: background-color 0.3s; + white-space: nowrap; +} + +#endTurnBtn:hover:not(:disabled) { + background: #45a049; +} + +#endTurnBtn:disabled { + background: #666; + cursor: not-allowed; +} + +#skipMovementBtn { + background: #2196F3; +} + +#skipMovementBtn:hover:not(:disabled) { + background: #1976D2; +} + +#skipMovementBtn:disabled { + background: #666; + cursor: not-allowed; +} + +#skipAttackBtn { + background: #FF9800; +} + +#skipAttackBtn:hover:not(:disabled) { + background: #F57C00; +} + +#skipAttackBtn:disabled { + background: #666; + cursor: not-allowed; +} + +/* Mobile optimizations */ +@media (max-width: 768px) { + #endTurnBtn { + padding: 12px 20px; + font-size: 16px; + top: 5px; + right: 5px; + } +} + +/* Touch-friendly interactions */ +@media (hover: none) and (pointer: coarse) { + #endTurnBtn { + min-height: 44px; /* iOS recommended minimum */ + } +} diff --git a/js/baba-yaga/HOST.md b/js/baba-yaga/HOST.md new file mode 100644 index 0000000..504ed96 --- /dev/null +++ b/js/baba-yaga/HOST.md @@ -0,0 +1,214 @@ +# Baba Yaga Host Application + +## Overview + +The Baba Yaga Host Application is a mobile-first, Smalltalk-inspired interactive development environment where users can create, extend, and run applications entirely within the Baba Yaga programming language. The application provides a complete development ecosystem with an infinite canvas, persistent floating action button (FAB), and integrated development tools. + +## Core Architecture + +### Infinite Canvas System +- **Free-form positioning** with optional grid snapping toggle +- **Grouping and containers** for organizing related elements +- **No zoom levels** - focused on simplicity and performance +- **Scrollable viewport** with smooth panning +- **Element management** with drag-and-drop positioning + +### Floating Action Button (FAB) +- **Persistent positioning** - always accessible regardless of canvas position +- **Context-aware actions** - shows relevant options based on current selection +- **Primary actions**: Add Code Editor, Interpreter, REPL, Project Explorer, Debugger +- **Secondary actions**: Create groups, add components, project operations + +## Project System + +### Project Format +- **Layered JSON/Binary Hybrid**: Core structure in JSON for git compatibility with custom serialization layer for performance +- **Custom Serialization**: Efficient encoding of Baba Yaga data structures with compression for large assets +- **Self-contained** - no external dependencies with embedded assets +- **Asset support**: Baba Yaga files, JSON/CSV data, optimized PNG images, audio files +- **Project metadata**: name, description, version, author, creation date, performance hints +- **Export/Import**: Local storage persistence with disk import/export and incremental saves + +### Project Structure +``` +project-name/ +├── project.json # Project metadata and configuration +├── src/ # Baba Yaga source files +│ ├── main.baba # Entry point +│ └── components/ # Custom components +├── assets/ # Data files, images, audio +├── tests/ # Test files +└── components/ # Shared component definitions +``` + +### Asset Management + +#### Image Assets +- **Format**: PNG-only with optimization constraints for mobile performance +- **Size Limits**: Maximum 1024x1024 pixels, recommended 512x512 for optimal performance +- **Color Constraints**: Limited to 256-color palette with optional dithering +- **Storage**: Base64-encoded in project JSON with compression for large images +- **Optimization**: Automatic palette reduction and dithering on import + +#### Image Integration with Baba Yaga +- **Image Type**: Native `Image` type with metadata (width, height, palette info) +- **Loading Functions**: `image.load(id)` and `image.loadAsync(id)` for asset access +- **Manipulation**: Basic operations like `image.scale`, `image.crop`, `image.flip` +- **Display**: Integration with rendering system for canvas display +- **Memory Management**: Reference counting with automatic cleanup + +#### Audio Assets (nice to have, not for version 1) +- **Format**: Compressed audio formats (MP3, OGG) with size limits +- **Storage**: Base64-encoded with optional streaming for large files +- **Baba Yaga Integration**: `audio.play(id)`, `audio.stop(id)`, `audio.volume(id, level)` functions + +## Component System + +### Built-in Components +- **Code Editor**: Line and row numbered Baba Yaga editor with live preview (syntax highlighting is a nice to have, but not with heavy dependencies) +- **Interpreter**: Runtime execution engine with error handling +- **REPL**: Interactive read-eval-print loop with command history +- **Project Explorer**: File/directory navigation and management +- **Debugger**: Error display, stack traces, and debugging tools +- **Testing Panel**: Built-in test runner and results display + +### Custom Components +- **Baba Yaga-powered widgets** - users create components using the language +- **Component sharing** via project exports +- **Event-driven communication** following functional programming principles +- **Immutable state management** consistent with Baba Yaga's functional style + +### Component Communication +- **FRP/Elm Hybrid Architecture**: Event-driven communication using Functional Reactive Programming principles combined with Elm-style model-view-update pattern +- **Baba Yaga Pattern Matching**: Event handlers implemented using `when` expressions with guards for sophisticated event routing +- **Immutable Event Streams**: Events as immutable data structures with functional transformations +- **Message passing** for inter-component communication via typed message protocols +- **Shared state** through immutable data structures with atomic updates +- **Functional composition** - components as pure functions with clear input/output contracts + +#### Event System Design +- **Event Bus**: Centralized pub/sub system with topic-based routing +- **Event Types**: Strongly typed events using Baba Yaga's type system +- **Event Filtering**: Pattern matching for selective event processing +- **Event Transformation**: Functional mapping and filtering of event streams +- **Error Handling**: Failed event handlers return `Err` values without crashing the system + +## Development Workflow + +### Live Coding +- **Immediate execution** of Baba Yaga code changes +- **Real-time preview** of component updates +- **Hot reloading** for rapid iteration +- **Error highlighting** with inline feedback + +### Debugging Experience +- **Clear error messages** with actionable guidance +- **Stack trace visualization** showing execution flow +- **Variable inspection** at breakpoints +- **Step-through debugging** for complex logic +- **Error recovery suggestions** and quick fixes + +### Testing Integration +- **Built-in test runner** with minimal setup +- **Test result display** in dedicated panel +- **Test-driven development** workflow support +- **Assertion library** integrated with Baba Yaga + +## Mobile-First Design + +### Responsive Interface +- **Adaptive layouts** for different screen sizes +- **Touch-friendly controls** with appropriate sizing +- **Orientation handling** for portrait and landscape +- **Accessibility features** for mobile users + +### Input Handling +- **Native HTML5 inputs** for keyboard input +- **Touch gestures** for canvas navigation +- **FAB-centric interaction** - all major actions through the floating button +- **Contextual menus** for secondary actions + +## Technical Implementation + +### Core Technologies +- **Web-based architecture** for cross-platform compatibility +- **Canvas API** for infinite scroll and element positioning +- **FRP Event System** for component communication with Baba Yaga pattern matching +- **Custom Serialization Layer** for efficient project data encoding +- **Local Storage** for project persistence with incremental saves +- **File API** for import/export operations +- **Web Workers** for background processing and image optimization + +### Performance Considerations +- **Virtual scrolling** for large canvases with many components +- **Lazy loading** of components and assets with demand-based initialization +- **Efficient rendering** with requestAnimationFrame and batched updates +- **Memory management** for long-running projects with garbage collection hints +- **Event Stream Optimization**: Debounced event processing and filtered subscriptions +- **Image Optimization**: Automatic compression, palette reduction, and progressive loading +- **Serialization Performance**: Efficient encoding/decoding with compression for large projects + +### Data Persistence +- **Auto-save** to local storage +- **Project export** to disk +- **Version history** with Smalltalk-style image snapshots +- **Incremental saves** for large projects + +## Project Templates + +### Starter Kits +- **Hello World**: Basic project structure +- **Component Library**: Pre-built UI components +- **Data Processing**: CSV/JSON handling examples +- **Game Development**: Simple game framework +- **Web App**: Basic web application template + +### Template System +- **Customizable templates** using Baba Yaga code +- **Template sharing** via project exports +- **Community templates** through shared projects +- **Template validation** and testing + +## Future Considerations + +### Potential Enhancements +- **Advanced FRP Features**: Time-travel debugging, event stream visualization, complex event processing +- **Cloud synchronization** for project backup with conflict resolution +- **Collaborative editing** for team projects with operational transformation +- **Plugin system** for extending functionality with Baba Yaga components +- **Performance profiling** tools for event streams and component rendering +- **Advanced debugging** features with FRP event tracing + +### Scalability +- **Large project support** with efficient memory usage +- **Component library management** for complex applications +- **Project optimization** tools +- **Bundle size optimization** for sharing + +## Development Priorities + +### Phase 1: Core Infrastructure +- Infinite canvas with basic positioning +- FAB system and core components +- Project format and persistence +- Basic Baba Yaga integration + +### Phase 2: Development Tools +- Code editor with syntax highlighting +- Interpreter and REPL integration +- Debugging and error handling +- Testing framework + +### Phase 3: User Experience +- Component creation and sharing +- Project templates and examples +- Mobile optimization +- Performance improvements + +### Phase 4: Advanced Features +- Custom component system +- Advanced debugging tools +- Project optimization +- Community features + +This architecture provides a solid foundation for a Smalltalk-inspired development environment while maintaining the functional programming principles of Baba Yaga and focusing on mobile-first user experience. diff --git a/js/baba-yaga/docs/ref.txt b/js/baba-yaga/docs/ref.txt index 40a7519..88320fe 100644 --- a/js/baba-yaga/docs/ref.txt +++ b/js/baba-yaga/docs/ref.txt @@ -143,7 +143,7 @@ FUNCTION COMBINATORS flip f // λx y. f y x apply f x // f x pipe x f // f x (reverse apply) -compose f g // λx. f (g x) +compose f g // λx. f (g x) (binary compose) VALIDATION & DEBUG ------------------ @@ -156,13 +156,27 @@ validate.email x // x is valid email // Debugging debug.print [name] value // print with optional name debug.inspect x // detailed inspection -assert condition message // throw if condition false +assert condition message // throw if condition false I/O --- io.out value // print value io.in // read stdin +JAVASCRIPT INTEROP +------------------ +io.callJS fnName args // call JS function synchronously +io.callJSAsync fnName args // call JS function asynchronously +io.getProperty obj propName // get JS object property +io.setProperty obj propName val // set JS object property +io.hasProperty obj propName // check if JS property exists +io.jsArrayToList jsArray // convert JS array to Baba Yaga list +io.listToJSArray list // convert Baba Yaga list to JS array +io.objectToTable jsObj // convert JS object to Baba Yaga table +io.tableToObject table // convert Baba Yaga table to JS object +io.getLastJSError // get last JS error (if available) +io.clearJSError // clear last JS error (if available) + EXAMPLES -------- // Fibonacci |