sam-coder-cli 1.0.45 → 1.0.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/agi-cli.js CHANGED
@@ -15,42 +15,8 @@ let OPENROUTER_API_KEY;
15
15
  let MODEL = 'deepseek/deepseek-chat-v3-0324:free';
16
16
  let API_BASE_URL = 'https://openrouter.ai/api/v1';
17
17
 
18
- // Tool/Function definitions for the AI
19
- // Knowledge management functions
20
- const KNOWLEDGE_FILE = path.join(os.homedir(), '.sam-coder-knowledge.json');
21
-
22
- async function loadKnowledge() {
23
- try {
24
- const data = await fs.readFile(KNOWLEDGE_FILE, 'utf8');
25
- return JSON.parse(data);
26
- } catch (error) {
27
- // If file doesn't exist, return empty knowledge
28
- return {};
29
- }
30
- }
31
-
32
- async function saveKnowledge(knowledge) {
33
- await fs.writeFile(KNOWLEDGE_FILE, JSON.stringify(knowledge, null, 2), 'utf8');
34
- }
35
-
36
18
  // Tool/Function definitions for the AI
37
19
  const tools = [
38
- {
39
- type: 'function',
40
- function: {
41
- name: 'addKnowledge',
42
- description: 'Add or update knowledge in the knowledge base',
43
- parameters: {
44
- type: 'object',
45
- properties: {
46
- key: { type: 'string', description: 'Key/identifier for the knowledge' },
47
- value: { type: 'string', description: 'The knowledge content' },
48
- description: { type: 'string', description: 'Description of what this knowledge is about' }
49
- },
50
- required: ['key', 'value', 'description']
51
- }
52
- }
53
- },
54
20
  {
55
21
  type: 'function',
56
22
  function: {
@@ -224,19 +190,12 @@ const tools = [
224
190
  // System prompt for the AI Assistant when using tool calling
225
191
  const TOOL_CALLING_PROMPT = `You are a helpful AI assistant with agency capabilities. You can perform actions on the user's system using the provided tools.
226
192
 
227
- KNOWLEDGE MANAGEMENT:
228
- - You can store and retrieve knowledge using the 'addKnowledge' tool.
229
- - Use this to remember user preferences, project details, or any other important information.
230
- - The knowledge will persist across sessions.
231
- - Example: If the user mentions they prefer Python for all projects, store this preference.
232
-
233
193
  TOOLS AVAILABLE:
234
- 1. addKnowledge - Add or update knowledge in the knowledge base
235
- 2. readFile - Read the contents of a file
236
- 3. writeFile - Write content to a file
237
- 4. editFile - Edit specific parts of a file
238
- 5. runCommand - Execute a shell command
239
- 6. searchFiles - Search for files using a glob pattern
194
+ 1. readFile - Read the contents of a file
195
+ 2. writeFile - Write content to a file
196
+ 3. editFile - Edit specific parts of a file
197
+ 4. runCommand - Execute a shell command
198
+ 5. searchFiles - Search for files using a glob pattern
240
199
 
241
200
  ENVIRONMENT:
242
201
  - OS: ${process.platform}
@@ -252,11 +211,130 @@ INSTRUCTIONS:
252
211
  Always think step by step and explain your reasoning before taking actions that could affect the system.`;
253
212
 
254
213
  // System prompt for the AI Assistant when using legacy function calling (JSON actions)
255
- const FUNCTION_CALLING_PROMPT = `You are a helpful AI assistant with agency capabilities. You can perform actions on the user's system.
214
+ const FUNCTION_CALLING_PROMPT = `You are a helpful AI assistant with agency capabilities. You can perform actions on the user's system using JSON-formatted actions.
215
+
216
+ AVAILABLE ACTIONS:
217
+ 1. read - Read the contents of a file
218
+ 2. write - Write content to a file
219
+ 3. edit - Edit specific parts of a file
220
+ 4. command - Execute a shell command
221
+ 5. search - Search for files using patterns
222
+ 6. execute - Execute code (bash or node.js)
223
+ 7. analyze - Analyze code and answer questions
224
+ 8. stop - Stop taking actions and provide final response
225
+
226
+ ACTION FORMAT:
227
+ You must respond with a JSON object wrapped in markdown code blocks with the 'json' language specifier.
228
+ The JSON should have the following structure:
229
+ {
230
+ "type": "action_name",
231
+ "data": { /* action-specific parameters */ },
232
+ "reasoning": "Brief explanation of why you're taking this action"
233
+ }
256
234
 
257
- By default, you will continue to take actions in a loop until you decide to stop with the 'stop' action type.
258
- Always wrap your JSON in markdown code blocks with the json language specifier.
259
- When executing code or commands that might be potentially harmful, explain what the code does before executing it.`;
235
+ EXAMPLES:
236
+
237
+ To read a file:
238
+ \`\`\`json
239
+ {
240
+ "type": "read",
241
+ "data": {
242
+ "path": "path/to/file.txt"
243
+ },
244
+ "reasoning": "Reading the file to understand its current contents"
245
+ }
246
+ \`\`\`
247
+
248
+ To write a file:
249
+ \`\`\`json
250
+ {
251
+ "type": "write",
252
+ "data": {
253
+ "path": "path/to/file.txt",
254
+ "content": "File content here"
255
+ },
256
+ "reasoning": "Creating a new file with the specified content"
257
+ }
258
+ \`\`\`
259
+
260
+ To edit a file:
261
+ \`\`\`json
262
+ {
263
+ "type": "edit",
264
+ "data": {
265
+ "path": "path/to/file.txt",
266
+ "edits": {
267
+ "operations": [
268
+ {
269
+ "type": "replace",
270
+ "startLine": 5,
271
+ "endLine": 7,
272
+ "newText": "New content for lines 5-7"
273
+ }
274
+ ]
275
+ }
276
+ },
277
+ "reasoning": "Modifying specific lines in the file"
278
+ }
279
+ \`\`\`
280
+
281
+ To run a command:
282
+ \`\`\`json
283
+ {
284
+ "type": "command",
285
+ "data": {
286
+ "command": "ls -la"
287
+ },
288
+ "reasoning": "Listing directory contents to see what files are available"
289
+ }
290
+ \`\`\`
291
+
292
+ To search for files:
293
+ \`\`\`json
294
+ {
295
+ "type": "search",
296
+ "data": {
297
+ "type": "files",
298
+ "pattern": "*.js"
299
+ },
300
+ "reasoning": "Finding all JavaScript files in the current directory"
301
+ }
302
+ \`\`\`
303
+
304
+ To execute code:
305
+ \`\`\`json
306
+ {
307
+ "type": "execute",
308
+ "data": {
309
+ "language": "bash",
310
+ "code": "echo 'Hello World'"
311
+ },
312
+ "reasoning": "Running a simple bash command"
313
+ }
314
+ \`\`\`
315
+
316
+ To stop and provide final response:
317
+ \`\`\`json
318
+ {
319
+ "type": "stop",
320
+ "data": {},
321
+ "reasoning": "Task completed successfully"
322
+ }
323
+ \`\`\`
324
+
325
+ IMPORTANT RULES:
326
+ - Always wrap your JSON in markdown code blocks with 'json' language specifier
327
+ - Include a "reasoning" field explaining why you're taking the action
328
+ - You will continue taking actions in a loop until you use the 'stop' action
329
+ - Be careful with destructive operations - explain what you're doing
330
+ - If an action fails, analyze the error and try a different approach
331
+ - Always think step by step before taking actions
332
+
333
+ ENVIRONMENT:
334
+ - OS: ${process.platform}
335
+ - Current directory: ${process.cwd()}
336
+
337
+ Start by understanding the user's request, then take appropriate actions to fulfill it.`;
260
338
 
261
339
  // Agent utilities
262
340
  const agentUtils = {
@@ -396,50 +474,70 @@ const agentUtils = {
396
474
  }
397
475
  };
398
476
 
399
- const toolHandlers = {
400
- addKnowledge: async ({ key, value, description }) => {
401
- try {
402
- const knowledge = await loadKnowledge();
403
- knowledge[key] = {
404
- value,
405
- description,
406
- timestamp: new Date().toISOString()
407
- };
408
- await saveKnowledge(knowledge);
409
- return { success: true, message: `Knowledge '${key}' added successfully` };
410
- } catch (error) {
411
- return { success: false, error: error.message };
412
- }
413
- },
414
- readFile,
415
- writeFile,
416
- editFile,
417
- runCommand,
418
- searchFiles
419
- };
420
-
421
477
  // Extract JSON from markdown code blocks
422
478
  function extractJsonFromMarkdown(text) {
423
- // Try to find a markdown code block with JSON content
424
- const codeBlockRegex = /```json\s*([\s\S]*?)\s*```/;
479
+ if (!text || typeof text !== 'string') {
480
+ console.error('Invalid input to extractJsonFromMarkdown:', typeof text);
481
+ return null;
482
+ }
483
+
484
+ // Try to find a markdown code block with JSON content (case insensitive)
485
+ const codeBlockRegex = /```(?:json|JSON)\s*([\s\S]*?)\s*```/i;
425
486
  const match = text.match(codeBlockRegex);
426
487
 
427
- if (match) {
488
+ if (match && match[1]) {
428
489
  try {
429
- return JSON.parse(match[1]);
490
+ const jsonStr = match[1].trim();
491
+ if (!jsonStr) {
492
+ console.error('Empty JSON content in markdown block');
493
+ return null;
494
+ }
495
+ return JSON.parse(jsonStr);
430
496
  } catch (error) {
431
- console.error('Error parsing JSON from markdown:', error);
497
+ console.error('Error parsing JSON from markdown block:', error.message);
498
+ console.error('JSON content was:', match[1]);
432
499
  }
433
500
  }
434
501
 
435
- // If no code block, try to parse the entire text as JSON
502
+ // If no code block found, look for JSON-like patterns in the text
503
+ const jsonPatterns = [
504
+ // Look for objects that start with { and end with }
505
+ /\{[\s\S]*?\}/g,
506
+ // Look for arrays that start with [ and end with ]
507
+ /\[[\s\S]*?\]/g
508
+ ];
509
+
510
+ for (const pattern of jsonPatterns) {
511
+ const matches = text.match(pattern);
512
+ if (matches) {
513
+ for (const match of matches) {
514
+ try {
515
+ const parsed = JSON.parse(match.trim());
516
+ // Validate that it looks like an action object
517
+ if (parsed && typeof parsed === 'object' && parsed.type) {
518
+ return parsed;
519
+ }
520
+ } catch (error) {
521
+ // Continue to next match
522
+ continue;
523
+ }
524
+ }
525
+ }
526
+ }
527
+
528
+ // If still no valid JSON found, try to parse the entire text as JSON
436
529
  try {
437
- return JSON.parse(text);
530
+ const trimmed = text.trim();
531
+ if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
532
+ return JSON.parse(trimmed);
533
+ }
438
534
  } catch (error) {
439
- console.error('Error parsing JSON:', error);
440
- ui.stopThinking();
441
- return null;
535
+ // Last resort failed
442
536
  }
537
+
538
+ console.error('Failed to extract valid JSON from response');
539
+ console.error('Response text was:', text);
540
+ return null;
443
541
  }
444
542
 
445
543
  // Call OpenRouter API with tool calling
@@ -491,29 +589,7 @@ async function callOpenRouter(messages, currentModel, useJson = false) {
491
589
  // Process a query with tool calling
492
590
  async function processQueryWithTools(query, conversation = [], currentModel) {
493
591
  const userMessage = { role: 'user', content: query };
494
- let messages = [...conversation, userMessage];
495
-
496
- // Add system message with knowledge if this is the first message
497
- if (conversation.length === 0) {
498
- // Load knowledge and include it in the system prompt
499
- const knowledge = await loadKnowledge();
500
- let systemPrompt = TOOL_CALLING_PROMPT;
501
-
502
- // Add knowledge to the system prompt if any exists
503
- const knowledgeEntries = Object.entries(knowledge);
504
- if (knowledgeEntries.length > 0) {
505
- systemPrompt += '\n\nCURRENT KNOWLEDGE BASE:\n';
506
- systemPrompt += knowledgeEntries.map(([key, { value, description }]) =>
507
- `- ${key}: ${value} (${description})`
508
- ).join('\n');
509
- }
510
-
511
- // Add system message at the beginning
512
- messages = [
513
- { role: 'system', content: systemPrompt },
514
- ...messages
515
- ];
516
- }
592
+ const messages = [...conversation, userMessage];
517
593
 
518
594
  ui.startThinking();
519
595
 
@@ -556,20 +632,12 @@ async function processQueryWithTools(query, conversation = [], currentModel) {
556
632
  async function handleToolCalls(toolCalls, messages) {
557
633
  const results = [];
558
634
 
559
- // Load knowledge at the start of processing tool calls
560
- const knowledge = await loadKnowledge();
561
-
562
635
  for (const toolCall of toolCalls) {
563
636
  const functionName = toolCall.function.name;
564
637
  let args;
565
638
 
566
639
  try {
567
640
  args = JSON.parse(toolCall.function.arguments);
568
-
569
- // If we have knowledge that might be relevant, include it in the context
570
- if (knowledge.defaultLanguage && !args.language) {
571
- args.language = knowledge.defaultLanguage.value;
572
- }
573
641
  } catch (error) {
574
642
  console.error('❌ Failed to parse tool arguments:', error);
575
643
  results.push({
@@ -635,10 +703,15 @@ async function executeAction(action) {
635
703
  }
636
704
  throw new Error('Text search is not implemented yet');
637
705
  case 'execute':
638
- const cmd = data.language === 'bash'
639
- ? data.code
640
- : `node -e "${data.code.replace(/"/g, '\"')}"`;
641
- return await agentUtils.runCommand(cmd);
706
+ if (data.language === 'bash' || data.language === 'sh') {
707
+ return await agentUtils.runCommand(data.code);
708
+ } else if (data.language === 'node' || data.language === 'javascript' || data.language === 'js') {
709
+ // For node.js code, escape quotes properly and handle multiline code
710
+ const escapedCode = data.code.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
711
+ return await agentUtils.runCommand(`node -e "${escapedCode}"`);
712
+ } else {
713
+ throw new Error(`Unsupported execution language: ${data.language}`);
714
+ }
642
715
  case 'browse':
643
716
  throw new Error('Web browsing is not implemented yet');
644
717
  case 'analyze':
@@ -653,44 +726,77 @@ async function executeAction(action) {
653
726
  async function processQuery(query, conversation = [], currentModel) {
654
727
  try {
655
728
  const userMessage = { role: 'user', content: query };
729
+
730
+ // Use existing conversation (which should already have system prompt from chat function)
656
731
  let messages = [...conversation, userMessage];
732
+
657
733
  let actionCount = 0;
658
734
  const MAX_ACTIONS = 10;
735
+ let finalResponse = '';
659
736
 
660
737
  ui.startThinking();
661
738
 
662
- const responseText = await callOpenRouter(messages, currentModel, true);
663
- const assistantMessage = responseText.choices[0].message;
664
- messages.push(assistantMessage);
665
-
666
- const actionData = extractJsonFromMarkdown(assistantMessage.content);
667
-
668
- if (!actionData || actionData.type === 'stop') {
669
- ui.stopThinking();
670
- return {
671
- response: assistantMessage.content,
672
- conversation: messages
673
- };
674
- }
675
-
676
- let currentAction = actionData;
677
- while (currentAction && currentAction.type !== 'stop' && actionCount < MAX_ACTIONS) {
678
- actionCount++;
679
- const result = await executeAction(currentAction);
739
+ while (actionCount < MAX_ACTIONS) {
740
+ const responseObj = await callOpenRouter(messages, currentModel, true);
741
+ const assistantMessage = responseObj.choices[0].message;
742
+ messages.push(assistantMessage);
680
743
 
681
- messages.push({ role: 'assistant', content: `Action result: ${result}` });
744
+ const actionData = extractJsonFromMarkdown(assistantMessage.content);
682
745
 
683
- ui.startThinking();
684
- const nextResponse = await callOpenRouter(messages, currentModel, true);
685
- const nextAssistantMessage = nextResponse.choices[0].message;
686
- messages.push(nextAssistantMessage);
746
+ // If no valid action found, treat as final response
747
+ if (!actionData || !actionData.type) {
748
+ finalResponse = assistantMessage.content;
749
+ break;
750
+ }
751
+
752
+ // If stop action, break with the reasoning or content
753
+ if (actionData.type === 'stop') {
754
+ finalResponse = actionData.reasoning || assistantMessage.content;
755
+ break;
756
+ }
757
+
758
+ actionCount++;
759
+ console.log(`🔧 Executing action ${actionCount}: ${actionData.type}`);
760
+ if (actionData.reasoning) {
761
+ console.log(`📝 Reasoning: ${actionData.reasoning}`);
762
+ }
687
763
 
688
- currentAction = extractJsonFromMarkdown(nextAssistantMessage.content);
764
+ try {
765
+ const result = await executeAction(actionData);
766
+ console.log('✅ Action executed successfully');
767
+
768
+ // Add action result to conversation
769
+ const resultMessage = {
770
+ role: 'user',
771
+ content: `Action result (${actionData.type}): ${result}`
772
+ };
773
+ messages.push(resultMessage);
774
+
775
+ } catch (error) {
776
+ console.error('❌ Action execution failed:', error.message);
777
+
778
+ // Add error result to conversation
779
+ const errorMessage = {
780
+ role: 'user',
781
+ content: `Action failed (${actionData.type}): ${error.message}`
782
+ };
783
+ messages.push(errorMessage);
784
+ }
785
+ }
786
+
787
+ // If we hit max actions, get a final response
788
+ if (actionCount >= MAX_ACTIONS && !finalResponse) {
789
+ const finalMsg = { role: 'user', content: 'Please provide a final summary of what was accomplished.' };
790
+ messages.push(finalMsg);
791
+
792
+ const finalResponseObj = await callOpenRouter(messages, currentModel, true);
793
+ finalResponse = finalResponseObj.choices[0].message.content;
794
+ messages.push(finalResponseObj.choices[0].message);
689
795
  }
690
796
 
691
797
  ui.stopThinking();
692
798
  return {
693
- response: messages[messages.length - 1].content,
799
+ response: finalResponse || 'Task completed.',
694
800
  conversation: messages
695
801
  };
696
802
  } catch (error) {
@@ -706,6 +812,14 @@ async function processQuery(query, conversation = [], currentModel) {
706
812
  async function chat(rl, useToolCalling, initialModel) {
707
813
  let currentModel = initialModel;
708
814
  const conversation = [];
815
+
816
+ // Initialize conversation with appropriate system prompt
817
+ if (useToolCalling) {
818
+ conversation.push({ role: 'system', content: TOOL_CALLING_PROMPT });
819
+ } else {
820
+ conversation.push({ role: 'system', content: FUNCTION_CALLING_PROMPT });
821
+ }
822
+
709
823
  console.log('Type your message, or "exit" to quit.');
710
824
 
711
825
  rl.setPrompt('> ');
@@ -740,28 +854,18 @@ async function chat(rl, useToolCalling, initialModel) {
740
854
  if (input.toLowerCase() === '/setup') {
741
855
  await runSetup(rl, true);
742
856
  console.log('\nSetup complete. Please restart the application to apply changes.');
743
- rl.prompt();
857
+ rl.close();
744
858
  return;
745
859
  }
746
-
747
- if (input.toLowerCase() === '/eraseknowledge') {
748
- try {
749
- await fs.unlink(KNOWLEDGE_FILE);
750
- console.log('✅ All knowledge has been erased.');
751
- } catch (error) {
752
- if (error.code === 'ENOENT') {
753
- console.log('ℹ️ No knowledge to erase.');
754
- } else {
755
- console.error('❌ Failed to erase knowledge:', error.message);
756
- }
757
- }
758
- rl.prompt();
860
+
861
+ if (input.toLowerCase() === 'exit') {
862
+ rl.close();
759
863
  return;
760
864
  }
761
865
 
762
866
  const result = useToolCalling
763
- ? await processQueryWithTools(query, conversation, currentModel)
764
- : await processQuery(query, conversation, currentModel);
867
+ ? await processQueryWithTools(input, conversation, currentModel)
868
+ : await processQuery(input, conversation, currentModel);
765
869
  ui.stopThinking();
766
870
  ui.showResponse(result.response);
767
871
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sam-coder-cli",
3
- "version": "1.0.45",
3
+ "version": "1.0.47",
4
4
  "description": "SAM-CODER: An animated command-line AI assistant with agency capabilities.",
5
5
  "main": "bin/agi-cli.js",
6
6
  "bin": {
@@ -1,218 +0,0 @@
1
- import pygame
2
- import random
3
-
4
- # Initialize pygame
5
- pygame.init()
6
-
7
- # Constants
8
- SCREEN_WIDTH = 300
9
- SCREEN_HEIGHT = 600
10
- BLOCK_SIZE = 30
11
- GRID_WIDTH = 10
12
- GRID_HEIGHT = 20
13
- GAME_AREA_LEFT = (SCREEN_WIDTH - GRID_WIDTH * BLOCK_SIZE) // 2
14
- GAME_AREA_TOP = SCREEN_HEIGHT - GRID_HEIGHT * BLOCK_SIZE
15
-
16
- # Colors
17
- BLACK = (0, 0, 0)
18
- WHITE = (255, 255, 255)
19
- GRAY = (128, 128, 128)
20
- COLORS = [
21
- (0, 255, 255), # Cyan (I)
22
- (0, 0, 255), # Blue (J)
23
- (255, 165, 0), # Orange (L)
24
- (255, 255, 0), # Yellow (O)
25
- (0, 255, 0), # Green (S)
26
- (128, 0, 128), # Purple (T)
27
- (255, 0, 0) # Red (Z)
28
- ]
29
-
30
- # Tetrimino shapes
31
- SHAPES = [
32
- [[1, 1, 1, 1]], # I
33
- [[1, 0, 0], [1, 1, 1]], # J
34
- [[0, 0, 1], [1, 1, 1]], # L
35
- [[1, 1], [1, 1]], # O
36
- [[0, 1, 1], [1, 1, 0]], # S
37
- [[0, 1, 0], [1, 1, 1]], # T
38
- [[1, 1, 0], [0, 1, 1]] # Z
39
- ]
40
-
41
- # Set up the display
42
- screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
43
- pygame.display.set_caption("Tetris")
44
-
45
- clock = pygame.time.Clock()
46
-
47
- # Game variables
48
- grid = [[0 for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
49
- current_piece = None
50
- next_piece = None
51
- current_x = GRID_WIDTH // 2
52
- current_y = 0
53
- score = 0
54
- game_over = False
55
- fall_time = 0
56
- fall_speed = 0.5 # seconds
57
-
58
- def new_piece():
59
- global current_piece, next_piece, current_x, current_y
60
- if next_piece is None:
61
- next_piece = random.randint(0, len(SHAPES) - 1)
62
- current_piece = next_piece
63
- next_piece = random.randint(0, len(SHAPES) - 1)
64
- current_x = GRID_WIDTH // 2 - len(SHAPES[current_piece][0]) // 2
65
- current_y = 0
66
- if check_collision():
67
- return False
68
- return True
69
-
70
- def check_collision():
71
- for y, row in enumerate(SHAPES[current_piece]):
72
- for x, cell in enumerate(row):
73
- if cell:
74
- if (current_y + y >= GRID_HEIGHT or
75
- current_x + x < 0 or
76
- current_x + x >= GRID_WIDTH or
77
- grid[current_y + y][current_x + x]):
78
- return True
79
- return False
80
-
81
- def merge_piece():
82
- for y, row in enumerate(SHAPES[current_piece]):
83
- for x, cell in enumerate(row):
84
- if cell:
85
- grid[current_y + y][current_x + x] = current_piece + 1
86
-
87
- def clear_lines():
88
- global grid, score
89
- lines_cleared = 0
90
- for y in range(GRID_HEIGHT - 1, -1, -1):
91
- if all(grid[y]):
92
- lines_cleared += 1
93
- for y2 in range(y, 0, -1):
94
- grid[y2] = grid[y2 - 1][:]
95
- grid[0] = [0] * GRID_WIDTH
96
- score += lines_cleared ** 2 * 100
97
-
98
- def draw_grid():
99
- for y in range(GRID_HEIGHT):
100
- for x in range(GRID_WIDTH):
101
- if grid[y][x]:
102
- pygame.draw.rect(
103
- screen, COLORS[grid[y][x] - 1],
104
- (GAME_AREA_LEFT + x * BLOCK_SIZE, GAME_AREA_TOP + y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)
105
- )
106
- pygame.draw.rect(
107
- screen, WHITE,
108
- (GAME_AREA_LEFT + x * BLOCK_SIZE, GAME_AREA_TOP + y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE),
109
- 1
110
- )
111
-
112
- def draw_piece():
113
- for y, row in enumerate(SHAPES[current_piece]):
114
- for x, cell in enumerate(row):
115
- if cell:
116
- pygame.draw.rect(
117
- screen, COLORS[current_piece],
118
- (GAME_AREA_LEFT + (current_x + x) * BLOCK_SIZE, GAME_AREA_TOP + (current_y + y) * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)
119
- )
120
- pygame.draw.rect(
121
- screen, WHITE,
122
- (GAME_AREA_LEFT + (current_x + x) * BLOCK_SIZE, GAME_AREA_TOP + (current_y + y) * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE),
123
- 1
124
- )
125
-
126
- def draw_next_piece():
127
- if next_piece is not None:
128
- font = pygame.font.SysFont(None, 24)
129
- text = font.render("Next:", True, WHITE)
130
- screen.blit(text, (20, 20))
131
- for y, row in enumerate(SHAPES[next_piece]):
132
- for x, cell in enumerate(row):
133
- if cell:
134
- pygame.draw.rect(
135
- screen, COLORS[next_piece],
136
- (40 + x * BLOCK_SIZE, 50 + y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)
137
- )
138
- pygame.draw.rect(
139
- screen, WHITE,
140
- (40 + x * BLOCK_SIZE, 50 + y * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE),
141
- 1
142
- )
143
-
144
- def draw_score():
145
- font = pygame.font.SysFont(None, 36)
146
- text = font.render(f"Score: {score}", True, WHITE)
147
- screen.blit(text, (20, SCREEN_HEIGHT - 50))
148
-
149
- def rotate_piece():
150
- global current_piece
151
- rotated = list(zip(*reversed(SHAPES[current_piece])))
152
- for y in range(len(rotated)):
153
- rotated[y] = list(rotated[y])
154
- old_shape = SHAPES[current_piece]
155
- SHAPES[current_piece] = rotated
156
- if check_collision():
157
- SHAPES[current_piece] = old_shape
158
-
159
- # Main game loop
160
- new_piece()
161
- running = True
162
- while running:
163
- screen.fill(BLACK)
164
- fall_time += clock.get_rawtime() / 1000 # Convert to seconds
165
- clock.tick()
166
-
167
- if fall_time >= fall_speed:
168
- fall_time = 0
169
- current_y += 1
170
- if check_collision():
171
- current_y -= 1
172
- merge_piece()
173
- clear_lines()
174
- if not new_piece():
175
- game_over = True
176
-
177
- for event in pygame.event.get():
178
- if event.type == pygame.QUIT:
179
- running = False
180
- if event.type == pygame.KEYDOWN:
181
- if event.key == pygame.K_LEFT:
182
- current_x -= 1
183
- if check_collision():
184
- current_x += 1
185
- if event.key == pygame.K_RIGHT:
186
- current_x += 1
187
- if check_collision():
188
- current_x -= 1
189
- if event.key == pygame.K_DOWN:
190
- current_y += 1
191
- if check_collision():
192
- current_y -= 1
193
- if event.key == pygame.K_UP:
194
- rotate_piece()
195
- if event.key == pygame.K_SPACE:
196
- while not check_collision():
197
- current_y += 1
198
- current_y -= 1
199
- merge_piece()
200
- clear_lines()
201
- if not new_piece():
202
- game_over = True
203
-
204
- draw_grid()
205
- draw_piece()
206
- draw_next_piece()
207
- draw_score()
208
- pygame.display.update()
209
-
210
- if game_over:
211
- font = pygame.font.SysFont(None, 48)
212
- text = font.render("GAME OVER", True, WHITE)
213
- screen.blit(text, (SCREEN_WIDTH // 2 - 100, SCREEN_HEIGHT // 2 - 24))
214
- pygame.display.update()
215
- pygame.time.wait(2000)
216
- running = False
217
-
218
- pygame.quit()