mirror of
https://github.com/MADTeacher/go_basics.git
synced 2025-11-23 21:34:47 +02:00
add new project step and small fix
This commit is contained in:
14
part_5/tic_tac_toe_v3/game/i_game_storage.go
Normal file
14
part_5/tic_tac_toe_v3/game/i_game_storage.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
type IGameLoader interface {
|
||||||
|
LoadGame(path string) (*Game, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IGameSaver interface {
|
||||||
|
SaveGame(path string, game *Game) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type IGameStorage interface {
|
||||||
|
IGameLoader
|
||||||
|
IGameSaver
|
||||||
|
}
|
||||||
50
part_5/tic_tac_toe_v3/game/json_game_storage.go
Normal file
50
part_5/tic_tac_toe_v3/game/json_game_storage.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewJsonGameStorage() IGameStorage {
|
||||||
|
return &JsonGameStorage{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type JsonGameStorage struct{}
|
||||||
|
|
||||||
|
func (j *JsonGameStorage) LoadGame(path string) (*Game, error) {
|
||||||
|
if !strings.HasSuffix(path, ".json") {
|
||||||
|
path += ".json"
|
||||||
|
}
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(file)
|
||||||
|
var game Game
|
||||||
|
err = decoder.Decode(&game)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &game, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *JsonGameStorage) SaveGame(path string, game *Game) error {
|
||||||
|
if !strings.HasSuffix(path, ".json") {
|
||||||
|
path += ".json"
|
||||||
|
}
|
||||||
|
file, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(file)
|
||||||
|
err = encoder.Encode(game)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
79
part_5/tic_tac_toe_v3/main.go
Normal file
79
part_5/tic_tac_toe_v3/main.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"tic-tac-toe/game"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
storage := game.NewJsonGameStorage()
|
||||||
|
boardSize := 0
|
||||||
|
|
||||||
|
for {
|
||||||
|
fmt.Println("1 - load game")
|
||||||
|
fmt.Println("2 - new game")
|
||||||
|
fmt.Println("q - quit")
|
||||||
|
|
||||||
|
input, _ := reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
switch input {
|
||||||
|
case "1":
|
||||||
|
var loadedGame *game.Game
|
||||||
|
var err error
|
||||||
|
for {
|
||||||
|
fmt.Println("Enter file name: ")
|
||||||
|
fileName, _ := reader.ReadString('\n')
|
||||||
|
fileName = strings.TrimSpace(fileName)
|
||||||
|
loadedGame, err = storage.LoadGame(fileName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error loading game.")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Для полного восстановления экземпляра игры
|
||||||
|
// присваиваем не дессериализуемым полям структуры
|
||||||
|
// необходимые значения
|
||||||
|
loadedGame.Reader = reader
|
||||||
|
loadedGame.Saver = storage.(game.IGameSaver)
|
||||||
|
loadedGame.Play() // Запускаем игру
|
||||||
|
case "2":
|
||||||
|
for {
|
||||||
|
fmt.Print("Enter the size of the board (3-9): ")
|
||||||
|
input, err := reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error reading input.")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
boardSize, err = strconv.Atoi(input)
|
||||||
|
if err != nil {
|
||||||
|
// Использовать предыдущий размер по умолчанию
|
||||||
|
boardSize = game.BoardDefaultSize
|
||||||
|
}
|
||||||
|
if boardSize < game.BoardMinSize ||
|
||||||
|
boardSize > game.BoardMaxSize {
|
||||||
|
fmt.Println("Invalid board size.")
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
board := game.NewBoard(boardSize)
|
||||||
|
player := game.NewPlayer()
|
||||||
|
game := game.NewGame(*board, *player, reader,
|
||||||
|
storage.(game.IGameSaver))
|
||||||
|
game.Play()
|
||||||
|
case "q":
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
fmt.Println("Invalid input. Please try again.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
part_5/tic_tac_toe_v4/.vscode/launch.json
vendored
Normal file
14
part_5/tic_tac_toe_v4/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Launch Package",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${fileDirname}", // <- ставим запятую
|
||||||
|
"console": "integratedTerminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
113
part_5/tic_tac_toe_v4/board/board.go
Normal file
113
part_5/tic_tac_toe_v4/board/board.go
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package board
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
BoardDefaultSize int = 3
|
||||||
|
BoardMinSize int = 3
|
||||||
|
BoardMaxSize int = 9
|
||||||
|
)
|
||||||
|
|
||||||
|
type Board struct {
|
||||||
|
Board [][]BoardField `json:"board"`
|
||||||
|
Size int `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBoard(size int) *Board {
|
||||||
|
board := make([][]BoardField, size)
|
||||||
|
for i := range board {
|
||||||
|
board[i] = make([]BoardField, size)
|
||||||
|
}
|
||||||
|
return &Board{Board: board, Size: size}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отображение игрового поля
|
||||||
|
func (b *Board) PrintBoard() {
|
||||||
|
fmt.Print(" ")
|
||||||
|
for i := range b.Size {
|
||||||
|
fmt.Printf("%d ", i+1)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
for i := range b.Size {
|
||||||
|
fmt.Printf("%d ", i+1)
|
||||||
|
for j := range b.Size {
|
||||||
|
switch b.Board[i][j] {
|
||||||
|
case Empty:
|
||||||
|
fmt.Print(". ")
|
||||||
|
case Cross:
|
||||||
|
fmt.Print("X ")
|
||||||
|
case Nought:
|
||||||
|
fmt.Print("O ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка возможности и выполнения хода
|
||||||
|
func (b *Board) makeMove(x, y int) bool {
|
||||||
|
return b.Board[x][y] == Empty
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Board) SetSymbol(x, y int, player BoardField) bool {
|
||||||
|
if b.makeMove(x, y) {
|
||||||
|
b.Board[x][y] = player
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка выигрыша
|
||||||
|
func (b *Board) CheckWin(player BoardField) bool {
|
||||||
|
// Проверка строк и столбцов
|
||||||
|
for i := range b.Size {
|
||||||
|
rowWin, colWin := true, true
|
||||||
|
for j := range b.Size {
|
||||||
|
if b.Board[i][j] != player {
|
||||||
|
rowWin = false
|
||||||
|
}
|
||||||
|
if b.Board[j][i] != player {
|
||||||
|
colWin = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rowWin || colWin {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Главная диагональ
|
||||||
|
mainDiag := true
|
||||||
|
for i := range b.Size {
|
||||||
|
if b.Board[i][i] != player {
|
||||||
|
mainDiag = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mainDiag {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Побочная диагональ
|
||||||
|
antiDiag := true
|
||||||
|
for i := range b.Size {
|
||||||
|
if b.Board[i][b.Size-i-1] != player {
|
||||||
|
antiDiag = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return antiDiag
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка на ничью
|
||||||
|
func (b *Board) CheckDraw() bool {
|
||||||
|
for i := range b.Size {
|
||||||
|
for j := range b.Size {
|
||||||
|
if b.Board[i][j] == Empty {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
10
part_5/tic_tac_toe_v4/board/board_cell_type.go
Normal file
10
part_5/tic_tac_toe_v4/board/board_cell_type.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package board
|
||||||
|
|
||||||
|
type BoardField int
|
||||||
|
|
||||||
|
// фигуры в клетке поля
|
||||||
|
const (
|
||||||
|
Empty BoardField = iota
|
||||||
|
Cross
|
||||||
|
Nought
|
||||||
|
)
|
||||||
79
part_5/tic_tac_toe_v4/game/game_core.go
Normal file
79
part_5/tic_tac_toe_v4/game/game_core.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
b "tic-tac-toe/board"
|
||||||
|
p "tic-tac-toe/player"
|
||||||
|
s "tic-tac-toe/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Game представляет состояние игры "Крестики-нолики"
|
||||||
|
type Game struct {
|
||||||
|
Board *b.Board `json:"board"`
|
||||||
|
Player p.IPlayer `json:"player"`
|
||||||
|
Player2 p.IPlayer `json:"-"` // Не сериализуется напрямую
|
||||||
|
CurrentPlayer p.IPlayer `json:"-"` // Не сериализуется напрямую
|
||||||
|
Reader *bufio.Reader `json:"-"`
|
||||||
|
State GameState `json:"state"`
|
||||||
|
Saver s.IGameSaver `json:"-"`
|
||||||
|
// Режим игры (PvP или PvC)
|
||||||
|
Mode GameMode `json:"mode"`
|
||||||
|
// Уровень сложности компьютера (только для PvC)
|
||||||
|
Difficulty p.Difficulty `json:"difficulty,omitempty"`
|
||||||
|
// Флаг для определения текущего игрока
|
||||||
|
IsCurrentFirst bool `json:"is_current_first"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGame создает новую игру
|
||||||
|
func NewGame(board b.Board, reader *bufio.Reader, saver s.IGameSaver,
|
||||||
|
mode GameMode, difficulty p.Difficulty) *Game {
|
||||||
|
// Создаем первого игрока (всегда человек на X)
|
||||||
|
player1 := p.NewHumanPlayer(b.Cross, reader)
|
||||||
|
|
||||||
|
var player2 p.IPlayer
|
||||||
|
if mode == PlayerVsPlayer {
|
||||||
|
// Для режима игрок против игрока создаем второго человека-игрока
|
||||||
|
player2 = p.NewHumanPlayer(b.Nought, reader)
|
||||||
|
} else {
|
||||||
|
// Для режима игрок против компьютера создаем компьютерного игрока
|
||||||
|
player2 = p.NewComputerPlayer(b.Nought, difficulty)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Game{
|
||||||
|
Board: &board,
|
||||||
|
Player: player1,
|
||||||
|
Player2: player2,
|
||||||
|
CurrentPlayer: player1,
|
||||||
|
Reader: reader,
|
||||||
|
State: playing,
|
||||||
|
Saver: saver,
|
||||||
|
Mode: mode,
|
||||||
|
Difficulty: difficulty,
|
||||||
|
IsCurrentFirst: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// switchCurrentPlayer переключает активного игрока
|
||||||
|
func (g *Game) switchCurrentPlayer() {
|
||||||
|
if g.CurrentPlayer == g.Player {
|
||||||
|
g.CurrentPlayer = g.Player2
|
||||||
|
} else {
|
||||||
|
g.CurrentPlayer = g.Player
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateState обновляет состояние игры на основе текущей доски
|
||||||
|
func (g *Game) updateState() {
|
||||||
|
if g.Board.CheckWin(g.CurrentPlayer.GetFigure()) {
|
||||||
|
if g.CurrentPlayer.GetFigure() == b.Cross {
|
||||||
|
g.State = crossWin
|
||||||
|
} else {
|
||||||
|
g.State = noughtWin
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.Board.CheckDraw() {
|
||||||
|
g.State = draw
|
||||||
|
}
|
||||||
|
}
|
||||||
127
part_5/tic_tac_toe_v4/game/game_play.go
Normal file
127
part_5/tic_tac_toe_v4/game/game_play.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
b "tic-tac-toe/board"
|
||||||
|
p "tic-tac-toe/player"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Play запускает игровой цикл
|
||||||
|
func (g *Game) Play() bool {
|
||||||
|
fmt.Println("For saving the game enter: save filename")
|
||||||
|
fmt.Println("For exiting the game enter : q")
|
||||||
|
fmt.Println("For making a move enter: row col")
|
||||||
|
|
||||||
|
for g.State == playing {
|
||||||
|
g.Board.PrintBoard()
|
||||||
|
|
||||||
|
// Определяем, кто делает ход: человек или компьютер
|
||||||
|
if g.Mode == PlayerVsComputer && g.CurrentPlayer == g.Player2 {
|
||||||
|
// Если ход компьютера, просто вызываем его MakeMove
|
||||||
|
fmt.Println("Computer is making a move...")
|
||||||
|
row, col, _ := g.CurrentPlayer.MakeMove(g.Board)
|
||||||
|
|
||||||
|
// Применяем ход компьютера к доске
|
||||||
|
g.Board.SetSymbol(row, col, g.CurrentPlayer.GetFigure())
|
||||||
|
} else {
|
||||||
|
// Если ход человека, запрашиваем ввод
|
||||||
|
figure := g.CurrentPlayer.GetFigure()
|
||||||
|
if figure == b.Cross {
|
||||||
|
fmt.Print("X move: ")
|
||||||
|
} else {
|
||||||
|
fmt.Print("O move: ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Читаем ввод пользователя
|
||||||
|
input, _ := g.Reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
|
||||||
|
// Проверка выхода из игры
|
||||||
|
if input == "q" {
|
||||||
|
g.State = quit
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка и выполнение сохранения игры
|
||||||
|
if g.saveCheck(input) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем ход человека-игрока через парсинг ввода
|
||||||
|
hPlayer, ok := g.CurrentPlayer.(*p.HumanPlayer)
|
||||||
|
if !ok {
|
||||||
|
fmt.Println("Invalide data. Please try again!")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим ввод и получаем координаты хода
|
||||||
|
row, col, validMove := hPlayer.ParseMove(input, g.Board)
|
||||||
|
if !validMove {
|
||||||
|
fmt.Println("Invalide data. Please try again!")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Устанавливаем символ на доску
|
||||||
|
if !g.Board.SetSymbol(row, col, hPlayer.GetFigure()) {
|
||||||
|
fmt.Println("This cell is already occupied!")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем состояние игры
|
||||||
|
g.updateState()
|
||||||
|
|
||||||
|
// Если игра продолжается, меняем игрока
|
||||||
|
if g.State == playing {
|
||||||
|
g.switchCurrentPlayer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Печатаем итоговую доску и результат
|
||||||
|
g.Board.PrintBoard()
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
switch g.State {
|
||||||
|
case crossWin:
|
||||||
|
fmt.Println("X wins!")
|
||||||
|
case noughtWin:
|
||||||
|
fmt.Println("O wins!")
|
||||||
|
case draw:
|
||||||
|
fmt.Println("It's a draw!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Возвращаем true, если игра закончилась нормально (не выходом)
|
||||||
|
return g.State != quit
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveCheck проверяет, является ли ввод командой сохранения
|
||||||
|
func (g *Game) saveCheck(input string) bool {
|
||||||
|
// Проверяем, если пользователь ввёл только "save" без имени файла
|
||||||
|
if input == "save" {
|
||||||
|
fmt.Println("Error: missing filename. " +
|
||||||
|
"Please use the format: save filename")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем команду сохранения с именем файла
|
||||||
|
if len(input) > 5 && input[:5] == "save " {
|
||||||
|
filename := input[5:]
|
||||||
|
|
||||||
|
// Проверяем, что имя файла не пустое
|
||||||
|
if len(strings.TrimSpace(filename)) == 0 {
|
||||||
|
fmt.Println("Error: empty file name. " +
|
||||||
|
"Please use the format: save filename")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Game saved to file: %s\n", filename)
|
||||||
|
shapshot := g.gameSnapshot()
|
||||||
|
if err := g.Saver.SaveGame(filename, shapshot); err != nil {
|
||||||
|
fmt.Printf("Error saving game: %v\n", err)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
82
part_5/tic_tac_toe_v4/game/game_serialization.go
Normal file
82
part_5/tic_tac_toe_v4/game/game_serialization.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
b "tic-tac-toe/board"
|
||||||
|
m "tic-tac-toe/model"
|
||||||
|
p "tic-tac-toe/player"
|
||||||
|
s "tic-tac-toe/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PrepareForSave подготавливает игру к сохранению
|
||||||
|
func (g *Game) PrepareForSave() {
|
||||||
|
// Устанавливаем флаг текущего игрока
|
||||||
|
g.IsCurrentFirst = (g.CurrentPlayer == g.Player)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) gameSnapshot() *m.GameSnapshot {
|
||||||
|
g.PrepareForSave()
|
||||||
|
return &m.GameSnapshot{
|
||||||
|
Board: g.Board,
|
||||||
|
PlayerFigure: g.Player.GetFigure(),
|
||||||
|
State: int(g.State),
|
||||||
|
Mode: int(g.Mode),
|
||||||
|
Difficulty: g.Difficulty,
|
||||||
|
IsCurrentFirst: g.IsCurrentFirst,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) RestoreFromSnapshot(
|
||||||
|
snapshot *m.GameSnapshot,
|
||||||
|
reader *bufio.Reader,
|
||||||
|
saver s.IGameSaver,
|
||||||
|
) {
|
||||||
|
g.Board = snapshot.Board
|
||||||
|
g.State = GameState(snapshot.State)
|
||||||
|
g.Mode = GameMode(snapshot.Mode)
|
||||||
|
g.Difficulty = snapshot.Difficulty
|
||||||
|
g.IsCurrentFirst = snapshot.IsCurrentFirst
|
||||||
|
|
||||||
|
// Создаем объекты игроков
|
||||||
|
g.Player = &p.HumanPlayer{Figure: snapshot.PlayerFigure}
|
||||||
|
|
||||||
|
g.Reader = reader
|
||||||
|
g.Saver = saver
|
||||||
|
|
||||||
|
g.recreatePlayersAfterLoad(reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RecreatePlayersAfterLoad восстанавливает объекты игроков после загрузки из JSON
|
||||||
|
func (g *Game) recreatePlayersAfterLoad(reader *bufio.Reader) {
|
||||||
|
// Создаем игроков в зависимости от режима игры
|
||||||
|
if g.Player == nil {
|
||||||
|
fmt.Println("Error: Player is nil")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
playerFigure := g.Player.GetFigure()
|
||||||
|
g.Player = p.NewHumanPlayer(playerFigure, reader)
|
||||||
|
|
||||||
|
// Получаем фигуру второго игрока
|
||||||
|
var player2Figure b.BoardField
|
||||||
|
if playerFigure == b.Cross {
|
||||||
|
player2Figure = b.Nought
|
||||||
|
} else {
|
||||||
|
player2Figure = b.Cross
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем второго игрока в зависимости от режима
|
||||||
|
if g.Mode == PlayerVsPlayer {
|
||||||
|
g.Player2 = p.NewHumanPlayer(player2Figure, reader)
|
||||||
|
} else {
|
||||||
|
g.Player2 = p.NewComputerPlayer(player2Figure, g.Difficulty)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Восстанавливаем указатель на текущего игрока
|
||||||
|
if g.IsCurrentFirst {
|
||||||
|
g.CurrentPlayer = g.Player
|
||||||
|
} else {
|
||||||
|
g.CurrentPlayer = g.Player2
|
||||||
|
}
|
||||||
|
}
|
||||||
116
part_5/tic_tac_toe_v4/game/game_setup.go
Normal file
116
part_5/tic_tac_toe_v4/game/game_setup.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
b "tic-tac-toe/board"
|
||||||
|
p "tic-tac-toe/player"
|
||||||
|
s "tic-tac-toe/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetupGame создает новую игру с пользовательскими настройками
|
||||||
|
func SetupGame(reader *bufio.Reader, saver s.IGameSaver) *Game {
|
||||||
|
// Запрашиваем размер игрового поля
|
||||||
|
size := getBoardSize(reader)
|
||||||
|
|
||||||
|
// Создаем доску
|
||||||
|
board := *b.NewBoard(size)
|
||||||
|
|
||||||
|
// Запрашиваем режим игры
|
||||||
|
mode := getGameMode(reader)
|
||||||
|
|
||||||
|
// Если выбран режим против компьютера, запрашиваем сложность
|
||||||
|
var difficulty p.Difficulty
|
||||||
|
if mode == PlayerVsComputer {
|
||||||
|
difficulty = getDifficulty(reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем новую игру
|
||||||
|
return NewGame(board, reader, saver, mode, difficulty)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getBoardSize запрашивает у пользователя размер доски
|
||||||
|
func getBoardSize(reader *bufio.Reader) int {
|
||||||
|
size := b.BoardDefaultSize
|
||||||
|
var err error
|
||||||
|
for {
|
||||||
|
fmt.Printf("Choose board size (min: %d, max: %d, default: %d): ",
|
||||||
|
b.BoardMinSize, b.BoardMaxSize, b.BoardDefaultSize)
|
||||||
|
|
||||||
|
input, _ := reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
|
||||||
|
// Если пользователь не ввел ничего, используем размер по умолчанию
|
||||||
|
if input == "" {
|
||||||
|
return b.BoardDefaultSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пытаемся преобразовать ввод в число
|
||||||
|
size, err = strconv.Atoi(input)
|
||||||
|
if err != nil || size < b.BoardMinSize || size > b.BoardMaxSize {
|
||||||
|
fmt.Println("Invalid input. Please try again!")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getGameMode запрашивает у пользователя режим игры
|
||||||
|
func getGameMode(reader *bufio.Reader) GameMode {
|
||||||
|
for {
|
||||||
|
fmt.Println("Choose game mode:")
|
||||||
|
fmt.Println("1 - Player vs Player (PvP)")
|
||||||
|
fmt.Println("2 - Player vs Computer (PvC)")
|
||||||
|
fmt.Print("Your choice: ")
|
||||||
|
|
||||||
|
input, err := reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Invalid input. Please try again!")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch input {
|
||||||
|
case "1":
|
||||||
|
return PlayerVsPlayer
|
||||||
|
case "2":
|
||||||
|
return PlayerVsComputer
|
||||||
|
default:
|
||||||
|
fmt.Println("Invalid input. Please try again!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDifficulty запрашивает у пользователя уровень сложности компьютера
|
||||||
|
func getDifficulty(reader *bufio.Reader) p.Difficulty {
|
||||||
|
for {
|
||||||
|
fmt.Println("Choose computer difficulty:")
|
||||||
|
fmt.Println("1 - Easy (random moves)")
|
||||||
|
fmt.Println("2 - Medium (block winning moves)")
|
||||||
|
fmt.Println("3 - Hard (optimal strategy)")
|
||||||
|
fmt.Print("Your choice: ")
|
||||||
|
|
||||||
|
input, err := reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Invalid input. Please try again!")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch input {
|
||||||
|
case "1":
|
||||||
|
return p.Easy
|
||||||
|
case "2":
|
||||||
|
return p.Medium
|
||||||
|
case "3":
|
||||||
|
return p.Hard
|
||||||
|
default:
|
||||||
|
fmt.Println("Invalid input. Please try again!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
part_5/tic_tac_toe_v4/game/game_state.go
Normal file
20
part_5/tic_tac_toe_v4/game/game_state.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
type GameState int
|
||||||
|
|
||||||
|
// состояние игрового процесса
|
||||||
|
const (
|
||||||
|
playing GameState = iota
|
||||||
|
draw
|
||||||
|
crossWin
|
||||||
|
noughtWin
|
||||||
|
quit
|
||||||
|
)
|
||||||
|
|
||||||
|
// Режим игры
|
||||||
|
type GameMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
PlayerVsPlayer GameMode = iota
|
||||||
|
PlayerVsComputer
|
||||||
|
)
|
||||||
3
part_5/tic_tac_toe_v4/go.mod
Normal file
3
part_5/tic_tac_toe_v4/go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module tic-tac-toe
|
||||||
|
|
||||||
|
go 1.24.0
|
||||||
69
part_5/tic_tac_toe_v4/main.go
Normal file
69
part_5/tic_tac_toe_v4/main.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"tic-tac-toe/game"
|
||||||
|
"tic-tac-toe/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
gameStorage := storage.NewJsonGameStorage()
|
||||||
|
|
||||||
|
for {
|
||||||
|
fmt.Println("Welcome to Tic-Tac-Toe!")
|
||||||
|
fmt.Println("1 - Load game")
|
||||||
|
fmt.Println("2 - New game")
|
||||||
|
fmt.Println("q - Exit")
|
||||||
|
fmt.Print("Your choice: ")
|
||||||
|
|
||||||
|
input, _ := reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
|
||||||
|
switch input {
|
||||||
|
case "1":
|
||||||
|
// Загрузка сохраненной игры
|
||||||
|
loadedGame := &game.Game{}
|
||||||
|
|
||||||
|
for {
|
||||||
|
fmt.Println("Input file name: ")
|
||||||
|
fileName, _ := reader.ReadString('\n')
|
||||||
|
fileName = strings.TrimSpace(fileName)
|
||||||
|
|
||||||
|
snapshote, err := gameStorage.LoadGame(fileName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error loading game: ", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Восстанавливаем все необходимые поля игры
|
||||||
|
loadedGame.RestoreFromSnapshot(
|
||||||
|
snapshote, reader,
|
||||||
|
gameStorage.(storage.IGameSaver),
|
||||||
|
)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запускаем игру
|
||||||
|
loadedGame.Play()
|
||||||
|
|
||||||
|
case "2":
|
||||||
|
// Создаем новую игру с помощью диалога настройки
|
||||||
|
newGame := game.SetupGame(reader, gameStorage.(storage.IGameSaver))
|
||||||
|
|
||||||
|
// Запускаем игру
|
||||||
|
newGame.Play()
|
||||||
|
|
||||||
|
case "q":
|
||||||
|
fmt.Println("Goodbye!")
|
||||||
|
return
|
||||||
|
|
||||||
|
default:
|
||||||
|
fmt.Println("Invalid choice. Please try again.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
part_5/tic_tac_toe_v4/model/game_snapshot.go
Normal file
16
part_5/tic_tac_toe_v4/model/game_snapshot.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
b "tic-tac-toe/board"
|
||||||
|
p "tic-tac-toe/player"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Структура для сериализации/десериализации игры
|
||||||
|
type GameSnapshot struct {
|
||||||
|
Board *b.Board `json:"board"`
|
||||||
|
PlayerFigure b.BoardField `json:"player_figure"`
|
||||||
|
State int `json:"state"`
|
||||||
|
Mode int `json:"mode"`
|
||||||
|
Difficulty p.Difficulty `json:"difficulty,omitempty"`
|
||||||
|
IsCurrentFirst bool `json:"is_current_first"`
|
||||||
|
}
|
||||||
310
part_5/tic_tac_toe_v4/player/computer_player.go
Normal file
310
part_5/tic_tac_toe_v4/player/computer_player.go
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
b "tic-tac-toe/board"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Уровни сложности компьютера
|
||||||
|
type Difficulty int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Easy Difficulty = iota
|
||||||
|
Medium
|
||||||
|
Hard
|
||||||
|
)
|
||||||
|
|
||||||
|
// ComputerPlayer представляет игрока-компьютера
|
||||||
|
type ComputerPlayer struct {
|
||||||
|
Figure b.BoardField `json:"figure"`
|
||||||
|
Difficulty Difficulty `json:"difficulty"`
|
||||||
|
rand *rand.Rand
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewComputerPlayer создает нового игрока-компьютера с заданным уровнем сложности
|
||||||
|
func NewComputerPlayer(
|
||||||
|
figure b.BoardField,
|
||||||
|
difficulty Difficulty,
|
||||||
|
) *ComputerPlayer {
|
||||||
|
source := rand.NewSource(time.Now().UnixNano())
|
||||||
|
return &ComputerPlayer{
|
||||||
|
Figure: figure,
|
||||||
|
Difficulty: difficulty,
|
||||||
|
rand: rand.New(source),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSymbol возвращает символ игрока
|
||||||
|
func (p *ComputerPlayer) GetSymbol() string {
|
||||||
|
if p.Figure == b.Cross {
|
||||||
|
return "X"
|
||||||
|
}
|
||||||
|
return "O"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SwitchPlayer изменяет фигуру текущего игрока
|
||||||
|
func (p *ComputerPlayer) SwitchPlayer() {
|
||||||
|
if p.Figure == b.Cross {
|
||||||
|
p.Figure = b.Nought
|
||||||
|
} else {
|
||||||
|
p.Figure = b.Cross
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFigure возвращает текущую фигуру игрока
|
||||||
|
func (p *ComputerPlayer) GetFigure() b.BoardField {
|
||||||
|
return p.Figure
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsComputer возвращает true для компьютера
|
||||||
|
func (p *ComputerPlayer) IsComputer() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeMove реализует ход компьютера в зависимости от выбранной сложности
|
||||||
|
func (p *ComputerPlayer) MakeMove(board *b.Board) (int, int, bool) {
|
||||||
|
fmt.Printf("%s (Computer) making move... ", p.GetSymbol())
|
||||||
|
|
||||||
|
var row, col int
|
||||||
|
switch p.Difficulty {
|
||||||
|
case Easy:
|
||||||
|
row, col = p.makeEasyMove(board)
|
||||||
|
case Medium:
|
||||||
|
row, col = p.makeMediumMove(board)
|
||||||
|
case Hard:
|
||||||
|
row, col = p.makeHardMove(board)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Move made (%d, %d)\n", row+1, col+1)
|
||||||
|
return row, col, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Легкий уровень: случайный ход на свободную клетку
|
||||||
|
func (p *ComputerPlayer) makeEasyMove(board *b.Board) (int, int) {
|
||||||
|
emptyCells := p.getEmptyCells(board)
|
||||||
|
if len(emptyCells) == 0 {
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выбираем случайную свободную клетку
|
||||||
|
randomIndex := p.rand.Intn(len(emptyCells))
|
||||||
|
return emptyCells[randomIndex][0], emptyCells[randomIndex][1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Средний уровень: проверяет возможность выигрыша
|
||||||
|
// или блокировки выигрыша противника
|
||||||
|
func (p *ComputerPlayer) makeMediumMove(board *b.Board) (int, int) {
|
||||||
|
// Проверяем, можем ли мы выиграть за один ход
|
||||||
|
if move := p.findWinningMove(board, p.Figure); move != nil {
|
||||||
|
return move[0], move[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, нужно ли блокировать победу противника
|
||||||
|
opponentFigure := b.Cross
|
||||||
|
if p.Figure == b.Cross {
|
||||||
|
opponentFigure = b.Nought
|
||||||
|
}
|
||||||
|
|
||||||
|
if move := p.findWinningMove(board, opponentFigure); move != nil {
|
||||||
|
return move[0], move[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Занимаем центр, если свободен (хорошая стратегия)
|
||||||
|
center := board.Size / 2
|
||||||
|
if board.Board[center][center] == b.Empty {
|
||||||
|
return center, center
|
||||||
|
}
|
||||||
|
|
||||||
|
// Занимаем угол, если свободен
|
||||||
|
corners := [][]int{
|
||||||
|
{0, 0},
|
||||||
|
{0, board.Size - 1},
|
||||||
|
{board.Size - 1, 0},
|
||||||
|
{board.Size - 1, board.Size - 1},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, corner := range corners {
|
||||||
|
if board.Board[corner[0]][corner[1]] == b.Empty {
|
||||||
|
return corner[0], corner[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если нет лучшего хода, делаем случайный ход
|
||||||
|
return p.makeEasyMove(board)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сложный уровень: использует алгоритм минимакс для оптимального хода
|
||||||
|
func (p *ComputerPlayer) makeHardMove(board *b.Board) (int, int) {
|
||||||
|
// Если доска пустая, ходим в центр или угол (оптимальный первый ход)
|
||||||
|
emptyCells := p.getEmptyCells(board)
|
||||||
|
if len(emptyCells) == board.Size*board.Size {
|
||||||
|
// Первый ход - центр или угол
|
||||||
|
center := board.Size / 2
|
||||||
|
// 50% шанс выбрать центр на нечетной доске
|
||||||
|
if p.rand.Intn(2) == 0 && board.Size%2 == 1 {
|
||||||
|
return center, center
|
||||||
|
} else {
|
||||||
|
corners := [][]int{
|
||||||
|
{0, 0},
|
||||||
|
{0, board.Size - 1},
|
||||||
|
{board.Size - 1, 0},
|
||||||
|
{board.Size - 1, board.Size - 1},
|
||||||
|
}
|
||||||
|
randomCorner := corners[p.rand.Intn(len(corners))]
|
||||||
|
return randomCorner[0], randomCorner[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для небольших досок (3x3) используем полный минимакс
|
||||||
|
if board.Size <= 3 {
|
||||||
|
bestScore := -1000
|
||||||
|
bestMove := []int{-1, -1}
|
||||||
|
|
||||||
|
// Рассматриваем все свободные клетки
|
||||||
|
for _, cell := range emptyCells {
|
||||||
|
row, col := cell[0], cell[1]
|
||||||
|
|
||||||
|
// Пробуем сделать ход
|
||||||
|
board.Board[row][col] = p.Figure
|
||||||
|
|
||||||
|
// Вычисляем оценку хода через минимакс
|
||||||
|
score := p.minimax(board, 0, false)
|
||||||
|
|
||||||
|
// Возвращаем клетку в исходное состояние
|
||||||
|
board.Board[row][col] = b.Empty
|
||||||
|
|
||||||
|
// Обновляем лучший ход
|
||||||
|
if score > bestScore {
|
||||||
|
bestScore = score
|
||||||
|
bestMove = []int{row, col}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestMove[0], bestMove[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для больших досок используем стратегию среднего уровня,
|
||||||
|
// так как полный минимакс будет слишком ресурсоемким
|
||||||
|
return p.makeMediumMove(board)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Алгоритм минимакс для определения оптимального хода
|
||||||
|
func (p *ComputerPlayer) minimax(
|
||||||
|
board *b.Board,
|
||||||
|
depth int, isMaximizing bool,
|
||||||
|
) int {
|
||||||
|
opponentFigure := b.Cross
|
||||||
|
if p.Figure == b.Cross {
|
||||||
|
opponentFigure = b.Nought
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем терминальное состояние
|
||||||
|
if board.CheckWin(p.Figure) {
|
||||||
|
return 10 - depth // Выигрыш, чем быстрее, тем лучше
|
||||||
|
} else if board.CheckWin(opponentFigure) {
|
||||||
|
return depth - 10 // Проигрыш, чем дольше, тем лучше
|
||||||
|
} else if board.CheckDraw() {
|
||||||
|
return 0 // Ничья
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyCells := p.getEmptyCells(board)
|
||||||
|
|
||||||
|
if isMaximizing {
|
||||||
|
bestScore := -1000
|
||||||
|
|
||||||
|
// Проходим по всем свободным клеткам
|
||||||
|
for _, cell := range emptyCells {
|
||||||
|
row, col := cell[0], cell[1]
|
||||||
|
|
||||||
|
// Делаем ход
|
||||||
|
board.Board[row][col] = p.Figure
|
||||||
|
|
||||||
|
// Рекурсивно оцениваем ход
|
||||||
|
score := p.minimax(board, depth+1, false)
|
||||||
|
|
||||||
|
// Отменяем ход
|
||||||
|
board.Board[row][col] = b.Empty
|
||||||
|
|
||||||
|
bestScore = max(score, bestScore)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestScore
|
||||||
|
} else {
|
||||||
|
bestScore := 1000
|
||||||
|
|
||||||
|
// Проходим по всем свободным клеткам
|
||||||
|
for _, cell := range emptyCells {
|
||||||
|
row, col := cell[0], cell[1]
|
||||||
|
|
||||||
|
// Делаем ход противника
|
||||||
|
board.Board[row][col] = opponentFigure
|
||||||
|
|
||||||
|
// Рекурсивно оцениваем ход
|
||||||
|
score := p.minimax(board, depth+1, true)
|
||||||
|
|
||||||
|
// Отменяем ход
|
||||||
|
board.Board[row][col] = b.Empty
|
||||||
|
|
||||||
|
bestScore = min(score, bestScore)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestScore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вспомогательная функция для поиска хода, приводящего к выигрышу
|
||||||
|
func (p *ComputerPlayer) findWinningMove(
|
||||||
|
board *b.Board,
|
||||||
|
figure b.BoardField,
|
||||||
|
) []int {
|
||||||
|
for _, cell := range p.getEmptyCells(board) {
|
||||||
|
row, col := cell[0], cell[1]
|
||||||
|
|
||||||
|
// Пробуем сделать ход
|
||||||
|
board.Board[row][col] = figure
|
||||||
|
|
||||||
|
// Проверяем, приведет ли этот ход к выигрышу
|
||||||
|
if board.CheckWin(figure) {
|
||||||
|
// Отменяем ход и возвращаем координаты
|
||||||
|
board.Board[row][col] = b.Empty
|
||||||
|
return []int{row, col}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отменяем ход
|
||||||
|
board.Board[row][col] = b.Empty
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil // Нет выигрышного хода
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получение списка пустых клеток
|
||||||
|
func (p *ComputerPlayer) getEmptyCells(board *b.Board) [][]int {
|
||||||
|
var emptyCells [][]int
|
||||||
|
|
||||||
|
for i := 0; i < board.Size; i++ {
|
||||||
|
for j := 0; j < board.Size; j++ {
|
||||||
|
if board.Board[i][j] == b.Empty {
|
||||||
|
emptyCells = append(emptyCells, []int{i, j})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return emptyCells
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вспомогательные функции max и min
|
||||||
|
func max(a, b int) int {
|
||||||
|
if a > b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
22
part_5/tic_tac_toe_v4/player/i_player.go
Normal file
22
part_5/tic_tac_toe_v4/player/i_player.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import b "tic-tac-toe/board"
|
||||||
|
|
||||||
|
// IPlayer представляет интерфейс для любого игрока (человека или компьютера)
|
||||||
|
type IPlayer interface {
|
||||||
|
// Получение символа игрока (X или O)
|
||||||
|
GetSymbol() string
|
||||||
|
|
||||||
|
// Переключение хода на другого игрока
|
||||||
|
SwitchPlayer()
|
||||||
|
|
||||||
|
// Получение текущей фигуры игрока
|
||||||
|
GetFigure() b.BoardField
|
||||||
|
|
||||||
|
// Выполнение хода игрока
|
||||||
|
// Возвращает координаты хода (x, y) и признак успешности
|
||||||
|
MakeMove(board *b.Board) (int, int, bool)
|
||||||
|
|
||||||
|
// Проверка, является ли игрок компьютером
|
||||||
|
IsComputer() bool
|
||||||
|
}
|
||||||
104
part_5/tic_tac_toe_v4/player/player.go
Normal file
104
part_5/tic_tac_toe_v4/player/player.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
b "tic-tac-toe/board"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HumanPlayer представляет игрока-человека
|
||||||
|
type HumanPlayer struct {
|
||||||
|
Figure b.BoardField `json:"figure"`
|
||||||
|
Reader *bufio.Reader `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHumanPlayer создает нового игрока-человека
|
||||||
|
func NewHumanPlayer(figure b.BoardField, reader *bufio.Reader) *HumanPlayer {
|
||||||
|
return &HumanPlayer{Figure: figure, Reader: reader}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSymbol возвращает символ игрока
|
||||||
|
func (p *HumanPlayer) GetSymbol() string {
|
||||||
|
if p.Figure == b.Cross {
|
||||||
|
return "X"
|
||||||
|
}
|
||||||
|
return "O"
|
||||||
|
}
|
||||||
|
|
||||||
|
// SwitchPlayer изменяет фигуру текущего игрока
|
||||||
|
func (p *HumanPlayer) SwitchPlayer() {
|
||||||
|
if p.Figure == b.Cross {
|
||||||
|
p.Figure = b.Nought
|
||||||
|
} else {
|
||||||
|
p.Figure = b.Cross
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFigure возвращает текущую фигуру игрока
|
||||||
|
func (p *HumanPlayer) GetFigure() b.BoardField {
|
||||||
|
return p.Figure
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeMove обрабатывает строку ввода от человека и преобразует её в координаты хода
|
||||||
|
// input - строка ввода в формате "1 2"
|
||||||
|
func (p *HumanPlayer) MakeMove(board *b.Board) (int, int, bool) {
|
||||||
|
fmt.Printf(
|
||||||
|
"%s's turn. Enter row and column (e.g. 1 2): ",
|
||||||
|
p.GetSymbol(),
|
||||||
|
)
|
||||||
|
|
||||||
|
input, err := p.Reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Invalid input. Please try again.")
|
||||||
|
return -1, -1, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.ParseMove(input, board)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseMove обрабатывает строку ввода от человека и преобразует её в координаты хода
|
||||||
|
func (p *HumanPlayer) ParseMove(
|
||||||
|
input string,
|
||||||
|
board *b.Board,
|
||||||
|
) (int, int, bool) {
|
||||||
|
parts := strings.Fields(input)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
fmt.Println("Invalid input. Please try again.")
|
||||||
|
return -1, -1, false
|
||||||
|
}
|
||||||
|
|
||||||
|
row, err1 := strconv.Atoi(parts[0])
|
||||||
|
col, err2 := strconv.Atoi(parts[1])
|
||||||
|
if err1 != nil || err2 != nil ||
|
||||||
|
row < 1 || col < 1 || row > board.Size ||
|
||||||
|
col > board.Size {
|
||||||
|
fmt.Println("Invalid input. Please try again.")
|
||||||
|
return -1, -1, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Преобразуем введенные координаты (начиная с 1) в индексы массива (начиная с 0)
|
||||||
|
return row - 1, col - 1, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsComputer возвращает false для человека-игрока
|
||||||
|
func (p *HumanPlayer) IsComputer() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для обратной совместимости
|
||||||
|
type Player HumanPlayer
|
||||||
|
|
||||||
|
func NewPlayer() *Player {
|
||||||
|
return (*Player)(NewHumanPlayer(b.Cross, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Player) SwitchPlayer() {
|
||||||
|
(*HumanPlayer)(p).SwitchPlayer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Player) GetSymbol() string {
|
||||||
|
return (*HumanPlayer)(p).GetSymbol()
|
||||||
|
}
|
||||||
1
part_5/tic_tac_toe_v4/rt.json
Normal file
1
part_5/tic_tac_toe_v4/rt.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"board":{"board":[[1,2,0],[0,1,0],[0,2,0]],"size":3},"player_figure":1,"state":0,"mode":1,"is_current_first":true}
|
||||||
16
part_5/tic_tac_toe_v4/storage/i_game_storage.go
Normal file
16
part_5/tic_tac_toe_v4/storage/i_game_storage.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import m "tic-tac-toe/model"
|
||||||
|
|
||||||
|
type IGameLoader interface {
|
||||||
|
LoadGame(path string) (*m.GameSnapshot, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IGameSaver interface {
|
||||||
|
SaveGame(path string, game *m.GameSnapshot) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type IGameStorage interface {
|
||||||
|
IGameLoader
|
||||||
|
IGameSaver
|
||||||
|
}
|
||||||
52
part_5/tic_tac_toe_v4/storage/json_game_storage.go
Normal file
52
part_5/tic_tac_toe_v4/storage/json_game_storage.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
m "tic-tac-toe/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewJsonGameStorage() IGameStorage {
|
||||||
|
return &JsonGameStorage{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type JsonGameStorage struct{}
|
||||||
|
|
||||||
|
func (j *JsonGameStorage) LoadGame(path string) (*m.GameSnapshot, error) {
|
||||||
|
if !strings.HasSuffix(path, ".json") {
|
||||||
|
path += ".json"
|
||||||
|
}
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(file)
|
||||||
|
var snapshot m.GameSnapshot
|
||||||
|
err = decoder.Decode(&snapshot)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &snapshot, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *JsonGameStorage) SaveGame(path string, game *m.GameSnapshot) error {
|
||||||
|
if !strings.HasSuffix(path, ".json") {
|
||||||
|
path += ".json"
|
||||||
|
}
|
||||||
|
file, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
encoder := json.NewEncoder(file)
|
||||||
|
err = encoder.Encode(game)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
13
part_6/6.1/golang/todo/.vscode/launch.json
vendored
Normal file
13
part_6/6.1/golang/todo/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Launch Go App",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "debug",
|
||||||
|
"program": "${workspaceFolder}",
|
||||||
|
"console": "integratedTerminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
94
part_6/6.1/golang/todo/db/db.go
Normal file
94
part_6/6.1/golang/todo/db/db.go
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SQLiteRepository struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDB(pathToDB string) *sql.DB {
|
||||||
|
db, err := sql.Open("sqlite3", pathToDB)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
db.Exec(ProjectTabelDefinition)
|
||||||
|
db.Exec(TaskTabelDefinition)
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSQLiteRepository() *SQLiteRepository {
|
||||||
|
var db *sql.DB
|
||||||
|
|
||||||
|
if _, err := os.Stat(dbName); os.IsNotExist(err) {
|
||||||
|
db = createDB(dbName)
|
||||||
|
fmt.Println("DB isn't exist")
|
||||||
|
putDefaultValuesToDB(&SQLiteRepository{
|
||||||
|
db: db,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
db, err = sql.Open("sqlite3", dbName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Println("DB already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SQLiteRepository{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func putDefaultValuesToDB(rep *SQLiteRepository) {
|
||||||
|
firstProject, _ := rep.AddProject(Project{
|
||||||
|
Name: "Go",
|
||||||
|
Description: "Roadmap for learning Go",
|
||||||
|
})
|
||||||
|
secondProject, _ := rep.AddProject(Project{
|
||||||
|
Name: "One Year",
|
||||||
|
Description: "Tasks for the year",
|
||||||
|
})
|
||||||
|
rep.AddTask(Task{
|
||||||
|
Name: "Variable",
|
||||||
|
Description: "Learning Go build-in variables",
|
||||||
|
Priority: 1,
|
||||||
|
}, firstProject.ID)
|
||||||
|
rep.AddTask(Task{
|
||||||
|
Name: "Struct",
|
||||||
|
Description: "Learning use struct in OOP code",
|
||||||
|
Priority: 3,
|
||||||
|
}, firstProject.ID)
|
||||||
|
rep.AddTask(Task{
|
||||||
|
Name: "Goroutine",
|
||||||
|
Description: "Learning concurrent programming",
|
||||||
|
Priority: 5,
|
||||||
|
}, firstProject.ID)
|
||||||
|
rep.AddTask(Task{
|
||||||
|
Name: "DataBase",
|
||||||
|
Description: "How write app with db",
|
||||||
|
Priority: 1,
|
||||||
|
}, firstProject.ID)
|
||||||
|
rep.AddTask(Task{
|
||||||
|
Name: "PhD",
|
||||||
|
Description: "Ph.D. in Technical Sciences",
|
||||||
|
Priority: 5,
|
||||||
|
}, secondProject.ID)
|
||||||
|
rep.AddTask(Task{
|
||||||
|
Name: "Losing weight",
|
||||||
|
Description: "Exercise and eat less chocolate",
|
||||||
|
Priority: 2,
|
||||||
|
}, secondProject.ID)
|
||||||
|
rep.AddTask(Task{
|
||||||
|
Name: "Пафос и превозмогание",
|
||||||
|
Description: "10к подписчиков на канале",
|
||||||
|
Priority: 2,
|
||||||
|
}, secondProject.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SQLiteRepository) Close() {
|
||||||
|
s.db.Close()
|
||||||
|
}
|
||||||
31
part_6/6.1/golang/todo/db/db_definition.go
Normal file
31
part_6/6.1/golang/todo/db/db_definition.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
const dbName = "todo.db"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrDuplicate = errors.New("record already exists")
|
||||||
|
ErrNotExists = errors.New("row not exists")
|
||||||
|
ErrUpdateFailed = errors.New("update failed")
|
||||||
|
ErrDeleteFailed = errors.New("delete failed")
|
||||||
|
)
|
||||||
|
|
||||||
|
const ProjectTabelDefinition = `
|
||||||
|
CREATE TABLE IF NOT EXISTS projects(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
name TEXT UNIQUE,
|
||||||
|
description TEXT
|
||||||
|
);
|
||||||
|
`
|
||||||
|
|
||||||
|
const TaskTabelDefinition = `
|
||||||
|
CREATE TABLE IF NOT EXISTS tasks(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
priority INTEGER NOT NULL,
|
||||||
|
is_done BOOLEAN NOT NULL CHECK (is_done IN (0, 1)),
|
||||||
|
project_id INTEGER not null references projects(id)
|
||||||
|
);
|
||||||
|
`
|
||||||
20
part_6/6.1/golang/todo/db/models.go
Normal file
20
part_6/6.1/golang/todo/db/models.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
type Project struct {
|
||||||
|
ID int
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Task struct {
|
||||||
|
ID int
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Priority uint8
|
||||||
|
IsDone bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectTask struct {
|
||||||
|
Task
|
||||||
|
ProjectID int
|
||||||
|
}
|
||||||
92
part_6/6.1/golang/todo/db/projects_crud.go
Normal file
92
part_6/6.1/golang/todo/db/projects_crud.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Метод для добавления проекта в базу данных
|
||||||
|
func (s *SQLiteRepository) AddProject(project Project) (*Project, error) {
|
||||||
|
res, err := s.db.Exec( // Запрос на добавление проекта
|
||||||
|
// Используем экранирование данных для предотвращения SQL-инъекции
|
||||||
|
// данные из переменных project.Name и project.Description
|
||||||
|
// будут вставлены в запрос в место знаков вопроса
|
||||||
|
// в пордке их перечисления
|
||||||
|
"INSERT INTO projects(name, description) values(?,?)",
|
||||||
|
project.Name, project.Description,
|
||||||
|
)
|
||||||
|
if err != nil { // Если произошла ошибка
|
||||||
|
var sqliteErr sqlite3.Error
|
||||||
|
|
||||||
|
// Если такой проект уже существует
|
||||||
|
if errors.As(err, &sqliteErr) {
|
||||||
|
if errors.Is(
|
||||||
|
sqliteErr.ExtendedCode,
|
||||||
|
sqlite3.ErrConstraintUnique) {
|
||||||
|
return nil, ErrDuplicate // Возвращаем ErrDuplicate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := res.LastInsertId() // Получаем ID
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
project.ID = int(id) // Устанавливаем ID проекту
|
||||||
|
|
||||||
|
return &project, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Метод для удаления проекта из базы данных
|
||||||
|
func (s *SQLiteRepository) DeleteProject(projectID int) error {
|
||||||
|
// Запрос на удаление проекта из таблицы projects
|
||||||
|
s.db.Exec("DELETE FROM projects WHERE id = ?", projectID)
|
||||||
|
// Запрос на удаление всех задач, связанных с проектом
|
||||||
|
res, err := s.db.Exec(
|
||||||
|
"DELETE FROM tasks WHERE project_id = ?",
|
||||||
|
projectID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, были ли удалены задачи
|
||||||
|
rowsAffected, err := res.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если не было удалено ни одной задачи
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return ErrDeleteFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Метод для получения всех проектов
|
||||||
|
func (s *SQLiteRepository) GetAllProjects() ([]Project, error) {
|
||||||
|
// Запрос на получение всех проектов
|
||||||
|
rows, err := s.db.Query("SELECT * FROM projects")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close() // Закрываем соединение
|
||||||
|
|
||||||
|
// Перебираем результаты запроса и добавляем в []Project
|
||||||
|
var projects []Project
|
||||||
|
for rows.Next() {
|
||||||
|
var project Project
|
||||||
|
// считываем данные из каждой строки, в соответствующие
|
||||||
|
// поля структуры Project
|
||||||
|
if err := rows.Scan(&project.ID, &project.Name,
|
||||||
|
&project.Description); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Добавляем проект в срез
|
||||||
|
projects = append(projects, project)
|
||||||
|
}
|
||||||
|
return projects, nil
|
||||||
|
}
|
||||||
129
part_6/6.1/golang/todo/db/tasks_crud.go
Normal file
129
part_6/6.1/golang/todo/db/tasks_crud.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// Метод для добавления задачи в базу данных
|
||||||
|
func (s *SQLiteRepository) AddTask(task Task, projectID int) (*Task, error) {
|
||||||
|
// Запрос на добавление задачи проекту с id == projectID
|
||||||
|
res, err := s.db.Exec(
|
||||||
|
"INSERT INTO tasks(name, description, priority,"+
|
||||||
|
" is_done, project_id) values(?,?,?,?,?)",
|
||||||
|
task.Name, task.Description, task.Priority,
|
||||||
|
task.IsDone, projectID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := res.LastInsertId() // Получаем ID
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
task.ID = int(id) // Устанавливаем ID задаче
|
||||||
|
|
||||||
|
return &task, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Метод для удаления задачи из базы данных
|
||||||
|
func (s *SQLiteRepository) DeleteTask(taskID int) error {
|
||||||
|
// Запрос на удаление задачи из таблицы tasks
|
||||||
|
res, err := s.db.Exec(
|
||||||
|
"DELETE FROM tasks WHERE id = ?",
|
||||||
|
taskID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, была ли удалена задача
|
||||||
|
rowsAffected, err := res.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если не была удалена ни одна задача
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return ErrDeleteFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Метод для получения всех задач
|
||||||
|
func (s *SQLiteRepository) GetAllTasks() (tasks []ProjectTask, err error) {
|
||||||
|
// Запрос на получение всех задач
|
||||||
|
rows, err := s.db.Query("SELECT * FROM tasks")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var task ProjectTask
|
||||||
|
// считываем данные из каждой строки, в соответствующие
|
||||||
|
// поля структуры ProjectTask
|
||||||
|
if err := rows.Scan(&task.ID, &task.Name,
|
||||||
|
&task.Description, &task.Priority,
|
||||||
|
&task.IsDone, &task.ProjectID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Добавляем задачу в срез
|
||||||
|
tasks = append(tasks, task)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Метод для получения всех задач конкретного проекта
|
||||||
|
func (s *SQLiteRepository) GetProjectTasks(projectID int) (tasks []Task, err error) {
|
||||||
|
// Запрос на получение всех задач у проекта с заданным id
|
||||||
|
rows, err := s.db.Query(
|
||||||
|
"SELECT * FROM tasks WHERE project_id = ?",
|
||||||
|
projectID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close() // Закрываем соединение
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var task Task
|
||||||
|
var progID int
|
||||||
|
// считываем данные из каждой строки, в соответствующие
|
||||||
|
// поля структуры Task
|
||||||
|
if err := rows.Scan(&task.ID, &task.Name, &task.Description,
|
||||||
|
&task.Priority, &task.IsDone, &progID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tasks = append(tasks, task)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Метод для обновления задачи
|
||||||
|
func (s *SQLiteRepository) TaskDone(taskId int) error {
|
||||||
|
if taskId == 0 { // Проверка на валидность
|
||||||
|
return errors.New("invalid updated ID")
|
||||||
|
}
|
||||||
|
// Запрос на перевод задачи с указанным id
|
||||||
|
// в состояние "выполнена"
|
||||||
|
res, err := s.db.Exec(
|
||||||
|
"UPDATE tasks SET is_done = ? WHERE id = ?", 1,
|
||||||
|
taskId,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, была ли обновлена задача
|
||||||
|
rowsAffected, err := res.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если не была обновлена ни одна задача
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return ErrUpdateFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
12
part_6/6.1/golang/todo/go.mod
Normal file
12
part_6/6.1/golang/todo/go.mod
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
module golang/todo
|
||||||
|
|
||||||
|
go 1.24
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/daviddengcn/go-colortext v1.0.0 // indirect
|
||||||
|
github.com/dixonwille/wmenu v4.0.2+incompatible // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
|
||||||
|
gopkg.in/dixonwille/wlog.v2 v2.0.0 // indirect
|
||||||
|
)
|
||||||
17
part_6/6.1/golang/todo/go.sum
Normal file
17
part_6/6.1/golang/todo/go.sum
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
github.com/daviddengcn/go-colortext v1.0.0 h1:ANqDyC0ys6qCSvuEK7l3g5RaehL/Xck9EX8ATG8oKsE=
|
||||||
|
github.com/daviddengcn/go-colortext v1.0.0/go.mod h1:zDqEI5NVUop5QPpVJUxE9UO10hRnmkD5G4Pmri9+m4c=
|
||||||
|
github.com/dixonwille/wmenu v4.0.2+incompatible h1:lxrPJsx9LpdUFD5T+dOfl6gPKLbBmiAtEdACLT1I2/w=
|
||||||
|
github.com/dixonwille/wmenu v4.0.2+incompatible/go.mod h1:DnajdZEKFQksxBctWekpWaQXQrDUHRBco6b8MyZnR1s=
|
||||||
|
github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho=
|
||||||
|
github.com/golangplus/bytes v1.0.0/go.mod h1:AdRaCFwmc/00ZzELMWb01soso6W1R/++O1XL80yAn+A=
|
||||||
|
github.com/golangplus/fmt v1.0.0/go.mod h1:zpM0OfbMCjPtd2qkTD/jX2MgiFCqklhSUFyDW44gVQE=
|
||||||
|
github.com/golangplus/testing v1.0.0/go.mod h1:ZDreixUV3YzhoVraIDyOzHrr76p6NUh6k/pPg/Q3gYA=
|
||||||
|
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
gopkg.in/dixonwille/wlog.v2 v2.0.0 h1:TbGWtD8ahWVSihKKr+z2Dw7Cv/7IrfN6dwrcrre17pU=
|
||||||
|
gopkg.in/dixonwille/wlog.v2 v2.0.0/go.mod h1:JYQHRnhGPLno/iATOiGkEXoRanJXqdz9Qo6/QwfARUc=
|
||||||
19
part_6/6.1/golang/todo/main.go
Normal file
19
part_6/6.1/golang/todo/main.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang/todo/db"
|
||||||
|
"golang/todo/menu"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Создаем репозиторий
|
||||||
|
rep := db.NewSQLiteRepository()
|
||||||
|
// Создаем отложенное закрытие соединения
|
||||||
|
defer rep.Close()
|
||||||
|
// Бесконечный цикл
|
||||||
|
for {
|
||||||
|
menu.CreateMenu(rep)
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
101
part_6/6.1/golang/todo/menu/menu.go
Normal file
101
part_6/6.1/golang/todo/menu/menu.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package menu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"golang/todo/db"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/dixonwille/wmenu"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Функция для создания меню
|
||||||
|
func CreateMenu(rep *db.SQLiteRepository) {
|
||||||
|
menu := wmenu.NewMenu("What would you like to do?")
|
||||||
|
|
||||||
|
menu.Action(func(opts []wmenu.Opt) error {
|
||||||
|
handleFunc(rep, opts)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
menu.Option("Add a new Project", 0, false, nil)
|
||||||
|
menu.Option("Delete a Project by ID", 1, false, nil)
|
||||||
|
menu.Option("Get all Projects", 2, false, nil)
|
||||||
|
menu.Option("Add a Task", 3, false, nil)
|
||||||
|
// Выбор по умолчанию. Если пользователь жмякнет Enter,
|
||||||
|
// не выбирая никакого пункта меню,
|
||||||
|
// то будет выполнен этот пункт
|
||||||
|
menu.Option("Get all Tasks", 4, true, nil)
|
||||||
|
menu.Option("Get all Project tasks", 5, false, nil)
|
||||||
|
menu.Option("Done a Task by ID", 6, false, nil)
|
||||||
|
menu.Option("Delete a Task by ID", 7, false, nil)
|
||||||
|
menu.Option("Quit Application", 8, false, nil)
|
||||||
|
menuerr := menu.Run()
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("---------------------------------")
|
||||||
|
if menuerr != nil {
|
||||||
|
log.Fatal(menuerr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для обработки ввода выбранного пункта меню
|
||||||
|
func handleFunc(rep *db.SQLiteRepository, opts []wmenu.Opt) {
|
||||||
|
switch opts[0].Value {
|
||||||
|
case 0:
|
||||||
|
fmt.Println("Adding a new Project")
|
||||||
|
addProject(rep)
|
||||||
|
case 1:
|
||||||
|
fmt.Println("Deleting a Project by ID")
|
||||||
|
deleteProjectByID(rep)
|
||||||
|
case 2:
|
||||||
|
fmt.Println("Getting all Projects")
|
||||||
|
getAllProjects(rep)
|
||||||
|
case 3:
|
||||||
|
fmt.Println("Adding a new Task")
|
||||||
|
addTask(rep)
|
||||||
|
case 4:
|
||||||
|
fmt.Println("Getting all Tasks")
|
||||||
|
getAllTasks(rep)
|
||||||
|
case 5:
|
||||||
|
fmt.Println("Getting all Project tasks by ProjectID")
|
||||||
|
getAllProjectTasks(rep)
|
||||||
|
case 6:
|
||||||
|
fmt.Println("Doing a Task by ID")
|
||||||
|
doneTask(rep)
|
||||||
|
case 7:
|
||||||
|
fmt.Println("Deleting a Task by ID")
|
||||||
|
deleteTaskByID(rep)
|
||||||
|
case 8:
|
||||||
|
fmt.Println("See you later!!!")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для вывода сообщения о невалидных данных
|
||||||
|
func printNotValidData() {
|
||||||
|
fmt.Println("Data is not valid!!!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для получения числового значения из потока ввода
|
||||||
|
func getIntValueFromStd(reader *bufio.Reader) (int, error) {
|
||||||
|
tempID, _, _ := reader.ReadLine()
|
||||||
|
idStr := strings.TrimSuffix(string(tempID), "\n")
|
||||||
|
idProj, err := strconv.Atoi(idStr)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return idProj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для получения строки из потока ввода
|
||||||
|
func getStringValueFromStd(reader *bufio.Reader) (string, error) {
|
||||||
|
data, err := reader.ReadString('\n')
|
||||||
|
data = strings.TrimSuffix(data, "\r\n")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
71
part_6/6.1/golang/todo/menu/project_handlers.go
Normal file
71
part_6/6.1/golang/todo/menu/project_handlers.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package menu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"golang/todo/db"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Функция для добавления нового проекта
|
||||||
|
func addProject(rep *db.SQLiteRepository) {
|
||||||
|
project := db.Project{} // Создаем новый проект
|
||||||
|
reader := bufio.NewReader(os.Stdin) // Создаем поток ввода
|
||||||
|
|
||||||
|
fmt.Print("Input project name: ")
|
||||||
|
name, _ := getStringValueFromStd(reader)
|
||||||
|
|
||||||
|
fmt.Print("Input description project: ")
|
||||||
|
desc, _ := getStringValueFromStd(reader)
|
||||||
|
|
||||||
|
project.Name = name
|
||||||
|
project.Description = desc
|
||||||
|
// Если название и описание проекта не пустые
|
||||||
|
if project.Name != "" && project.Description != "" {
|
||||||
|
project, err := rep.AddProject(project) // Добавляем проект
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
} else {
|
||||||
|
// Выводим информацию о добавленном проекте
|
||||||
|
fmt.Printf("\nAdded project: %+v\n", *project)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Выводим сообщение об ошибке
|
||||||
|
printNotValidData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для удаления проекта
|
||||||
|
func deleteProjectByID(rep *db.SQLiteRepository) {
|
||||||
|
fmt.Print("Input ID for deleting project: ")
|
||||||
|
id, err := getIntValueFromStd(bufio.NewReader(os.Stdin))
|
||||||
|
if err != nil {
|
||||||
|
printNotValidData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rep.DeleteProject(id)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Project deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для получения всех проектов
|
||||||
|
func getAllProjects(rep *db.SQLiteRepository) {
|
||||||
|
progects, err := rep.GetAllProjects()
|
||||||
|
if err != nil {
|
||||||
|
printNotValidData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(progects) == 0 {
|
||||||
|
fmt.Println("You don't have any project")
|
||||||
|
} else {
|
||||||
|
fmt.Println("You current projects:")
|
||||||
|
for _, it := range progects {
|
||||||
|
fmt.Printf("ProjectID: %v || Name: %v || Desc: %v\n",
|
||||||
|
it.ID, it.Name, it.Description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
136
part_6/6.1/golang/todo/menu/task_handlers.go
Normal file
136
part_6/6.1/golang/todo/menu/task_handlers.go
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
package menu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"golang/todo/db"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Функция для добавления новой задачи
|
||||||
|
func addTask(rep *db.SQLiteRepository) {
|
||||||
|
task := db.Task{}
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
|
fmt.Print("Input project ID: ")
|
||||||
|
projectID, err := getIntValueFromStd(reader)
|
||||||
|
if err != nil {
|
||||||
|
printNotValidData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print("Input task name: ")
|
||||||
|
name, _ := getStringValueFromStd(reader)
|
||||||
|
|
||||||
|
fmt.Print("Input description task: ")
|
||||||
|
desc, _ := getStringValueFromStd(reader)
|
||||||
|
|
||||||
|
fmt.Print("Input priority task: ")
|
||||||
|
priority, err := getIntValueFromStd(reader)
|
||||||
|
if err != nil {
|
||||||
|
printNotValidData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
task.Name = name
|
||||||
|
task.Description = desc
|
||||||
|
task.Priority = uint8(priority)
|
||||||
|
if task.Name != "" && task.Description != "" {
|
||||||
|
task, err := rep.AddTask(task, projectID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("\nAdded task: %+v\n", *task)
|
||||||
|
} else {
|
||||||
|
printNotValidData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для удаления задачи
|
||||||
|
func deleteTaskByID(rep *db.SQLiteRepository) {
|
||||||
|
fmt.Print("Input ID for deleting task: ")
|
||||||
|
id, err := getIntValueFromStd(bufio.NewReader(os.Stdin))
|
||||||
|
if err != nil {
|
||||||
|
printNotValidData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rep.DeleteTask(id)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Task deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для получения всех задач
|
||||||
|
func getAllTasks(rep *db.SQLiteRepository) {
|
||||||
|
tasks, err := rep.GetAllTasks()
|
||||||
|
if err != nil {
|
||||||
|
printNotValidData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(tasks) == 0 {
|
||||||
|
fmt.Println("You don't have any task")
|
||||||
|
} else {
|
||||||
|
fmt.Println("You current tasks: ")
|
||||||
|
for _, it := range tasks {
|
||||||
|
fmt.Printf("TaskID: %v || Name: %v || Desc: %v ||"+
|
||||||
|
" Priority: %v || IsDone: %v || ProjID: %v\n",
|
||||||
|
it.ID, it.Name, it.Description, it.Priority,
|
||||||
|
it.IsDone, it.ProjectID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для получения всех задач проекта
|
||||||
|
func getAllProjectTasks(rep *db.SQLiteRepository) {
|
||||||
|
fmt.Print("Input ID for project: ")
|
||||||
|
|
||||||
|
id, err := getIntValueFromStd(bufio.NewReader(os.Stdin))
|
||||||
|
if err != nil {
|
||||||
|
printNotValidData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks, err := rep.GetProjectTasks(id)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tasks) == 0 {
|
||||||
|
fmt.Println("You don't have any task")
|
||||||
|
} else {
|
||||||
|
fmt.Printf(
|
||||||
|
"Project with ID = %d have next tasks:\n",
|
||||||
|
id,
|
||||||
|
)
|
||||||
|
for _, it := range tasks {
|
||||||
|
fmt.Printf("TaskID: %v || Name: %v || Desc: %v ||"+
|
||||||
|
" Priority: %v || IsDone: %v\n",
|
||||||
|
it.ID, it.Name, it.Description,
|
||||||
|
it.Priority, it.IsDone,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Функция для перевода задачи в состояние "Выполнено"
|
||||||
|
func doneTask(rep *db.SQLiteRepository) {
|
||||||
|
fmt.Print("Input task ID: ")
|
||||||
|
|
||||||
|
id, err := getIntValueFromStd(bufio.NewReader(os.Stdin))
|
||||||
|
if err != nil {
|
||||||
|
printNotValidData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rep.TaskDone(id)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Congratulations! Task done!")
|
||||||
|
}
|
||||||
BIN
part_6/6.1/golang/todo/todo.db
Normal file
BIN
part_6/6.1/golang/todo/todo.db
Normal file
Binary file not shown.
94
part_6/6.2/golang/todo_gorm/db/db.go
Normal file
94
part_6/6.2/golang/todo_gorm/db/db.go
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SQLiteRepository struct {
|
||||||
|
db *gorm.DB // заменили на *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSQLiteRepository() *SQLiteRepository {
|
||||||
|
var db *gorm.DB
|
||||||
|
rep := &SQLiteRepository{}
|
||||||
|
// Если база данных не существует, то создаем ее
|
||||||
|
if _, err := os.Stat(dbName); os.IsNotExist(err) {
|
||||||
|
// Отквываем соединение с базой данных
|
||||||
|
db, err = gorm.Open(sqlite.Open(dbName), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Println("DB isn't exist")
|
||||||
|
// Создаем таблицы
|
||||||
|
db.AutoMigrate(&Project{}, &ProjectTask{})
|
||||||
|
// Заполняем БД значениями по умолчанию
|
||||||
|
rep.db = db
|
||||||
|
putDefaultValuesToDB(rep)
|
||||||
|
} else {
|
||||||
|
// Отквываем соединение с базой данных
|
||||||
|
db, err = gorm.Open(sqlite.Open(dbName), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
rep.db = db
|
||||||
|
fmt.Println("DB already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
return rep
|
||||||
|
}
|
||||||
|
|
||||||
|
func putDefaultValuesToDB(rep *SQLiteRepository) {
|
||||||
|
firstProject, _ := rep.AddProject(Project{
|
||||||
|
Name: "Go",
|
||||||
|
Description: "Roadmap for learning Go",
|
||||||
|
})
|
||||||
|
secondProject, _ := rep.AddProject(Project{
|
||||||
|
Name: "One Year",
|
||||||
|
Description: "Tasks for the year",
|
||||||
|
})
|
||||||
|
rep.AddTask(Task{
|
||||||
|
Name: "Variable",
|
||||||
|
Description: "Learning Go build-in variables",
|
||||||
|
Priority: 1,
|
||||||
|
}, firstProject.ID)
|
||||||
|
rep.AddTask(Task{
|
||||||
|
Name: "Struct",
|
||||||
|
Description: "Learning use struct in OOP code",
|
||||||
|
Priority: 3,
|
||||||
|
}, firstProject.ID)
|
||||||
|
rep.AddTask(Task{
|
||||||
|
Name: "Goroutine",
|
||||||
|
Description: "Learning concurrent programming",
|
||||||
|
Priority: 5,
|
||||||
|
}, firstProject.ID)
|
||||||
|
rep.AddTask(Task{
|
||||||
|
Name: "DataBase",
|
||||||
|
Description: "How write app with db",
|
||||||
|
Priority: 1,
|
||||||
|
}, firstProject.ID)
|
||||||
|
rep.AddTask(Task{
|
||||||
|
Name: "PhD",
|
||||||
|
Description: "Ph.D. in Technical Sciences",
|
||||||
|
Priority: 5,
|
||||||
|
}, secondProject.ID)
|
||||||
|
rep.AddTask(Task{
|
||||||
|
Name: "Losing weight",
|
||||||
|
Description: "Exercise and eat less chocolate",
|
||||||
|
Priority: 2,
|
||||||
|
}, secondProject.ID)
|
||||||
|
rep.AddTask(Task{
|
||||||
|
Name: "Пафос и превозмогание",
|
||||||
|
Description: "10к подписчиков на канале",
|
||||||
|
Priority: 2,
|
||||||
|
}, secondProject.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SQLiteRepository) Close() {
|
||||||
|
|
||||||
|
}
|
||||||
12
part_6/6.2/golang/todo_gorm/db/db_definition.go
Normal file
12
part_6/6.2/golang/todo_gorm/db/db_definition.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
const dbName = "todo.db"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrDuplicate = errors.New("record already exists")
|
||||||
|
ErrNotExists = errors.New("row not exists")
|
||||||
|
ErrUpdateFailed = errors.New("update failed")
|
||||||
|
ErrDeleteFailed = errors.New("delete failed")
|
||||||
|
)
|
||||||
21
part_6/6.2/golang/todo_gorm/db/models.go
Normal file
21
part_6/6.2/golang/todo_gorm/db/models.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
type Project struct {
|
||||||
|
ID int `gorm:"primary_key;autoIncrement:true;not null"`
|
||||||
|
Name string `gorm:"unique;not null"`
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Task struct {
|
||||||
|
ID int `gorm:"primary_key;autoIncrement;not null"`
|
||||||
|
Name string `gorm:"not null"`
|
||||||
|
Description string `gorm:"not null"`
|
||||||
|
Priority uint8 `gorm:"not null"`
|
||||||
|
IsDone bool `gorm:"not null"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectTask struct {
|
||||||
|
Task
|
||||||
|
ProjectID int `gorm:"not null"`
|
||||||
|
Project *Project `gorm:"foreignKey:ProjectID;references:ID"`
|
||||||
|
}
|
||||||
53
part_6/6.2/golang/todo_gorm/db/projects_crud.go
Normal file
53
part_6/6.2/golang/todo_gorm/db/projects_crud.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Метод для добавления проекта в базу данных
|
||||||
|
func (r *SQLiteRepository) AddProject(project Project) (*Project, error) {
|
||||||
|
tx := r.db.Create(&project)
|
||||||
|
if tx.Error != nil {
|
||||||
|
var sqliteErr sqlite3.Error
|
||||||
|
if errors.As(tx.Error, &sqliteErr) {
|
||||||
|
if errors.Is(sqliteErr.ExtendedCode,
|
||||||
|
sqlite3.ErrConstraintUnique) {
|
||||||
|
return nil, ErrDuplicate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, tx.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return &project, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Метод для удаления проекта из базы данных
|
||||||
|
func (r *SQLiteRepository) DeleteProject(projectID int) error {
|
||||||
|
tx := r.db.Delete(&Project{ID: projectID})
|
||||||
|
if tx.Error != nil {
|
||||||
|
return tx.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected := tx.RowsAffected
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return ErrDeleteFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Метод для получения всех проектов
|
||||||
|
func (r *SQLiteRepository) GetAllProjects() ([]Project, error) {
|
||||||
|
var projects []Project
|
||||||
|
tx := r.db.Find(&projects)
|
||||||
|
if tx.Error != nil {
|
||||||
|
return nil, tx.Error
|
||||||
|
}
|
||||||
|
if tx.RowsAffected == 0 {
|
||||||
|
return nil, ErrNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
return projects, nil
|
||||||
|
}
|
||||||
95
part_6/6.2/golang/todo_gorm/db/tasks_crud.go
Normal file
95
part_6/6.2/golang/todo_gorm/db/tasks_crud.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// Метод для добавления задачи в базу данных
|
||||||
|
func (r *SQLiteRepository) AddTask(task Task, projectID int) (*Task, error) {
|
||||||
|
pjTask := &ProjectTask{ // создаем связь между задачей и проектом
|
||||||
|
Task: task,
|
||||||
|
ProjectID: projectID,
|
||||||
|
}
|
||||||
|
tx := r.db.Create(pjTask) // создаем задачу
|
||||||
|
if tx.Error != nil {
|
||||||
|
return nil, tx.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
return &pjTask.Task, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Метод для удаления задачи из базы данных
|
||||||
|
func (r *SQLiteRepository) DeleteTask(taskID int) error {
|
||||||
|
// удаляем задачу по ее ID
|
||||||
|
tx := r.db.Delete(&ProjectTask{Task: Task{ID: taskID}})
|
||||||
|
if tx.Error != nil {
|
||||||
|
return tx.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
rowsAffected := tx.RowsAffected
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return ErrDeleteFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Метод для получения всех задач
|
||||||
|
func (r *SQLiteRepository) GetAllTasks() (tasks []ProjectTask, err error) {
|
||||||
|
tx := r.db.Find(&tasks) // получаем все задачи
|
||||||
|
if tx.Error != nil {
|
||||||
|
return nil, tx.Error
|
||||||
|
}
|
||||||
|
if tx.RowsAffected == 0 {
|
||||||
|
return nil, ErrNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Метод для получения всех задач конкретного проекта
|
||||||
|
func (r *SQLiteRepository) GetProjectTasks(projectID int) (tasks []Task, err error) {
|
||||||
|
if projectID == 0 {
|
||||||
|
return nil, errors.New("invalid updated ID")
|
||||||
|
}
|
||||||
|
|
||||||
|
var pjTasks []ProjectTask
|
||||||
|
// получаем все задачи конкретного проекта
|
||||||
|
// и добавляем их в срез pjTasks []ProjectTask
|
||||||
|
tx := r.db.Where("project_id", projectID).Find(&pjTasks)
|
||||||
|
if tx.Error != nil {
|
||||||
|
return nil, tx.Error
|
||||||
|
}
|
||||||
|
if tx.RowsAffected == 0 {
|
||||||
|
return nil, ErrNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, it := range pjTasks {
|
||||||
|
// Отделяем задачу от проекта
|
||||||
|
// и добавляем в срез tasks
|
||||||
|
tasks = append(tasks, it.Task)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Метод для обновления задачи
|
||||||
|
func (r *SQLiteRepository) TaskDone(taskId int) error {
|
||||||
|
if taskId == 0 { // Проверка на валидность
|
||||||
|
return errors.New("invalid updated ID")
|
||||||
|
}
|
||||||
|
// Поиск задачи по ее ID. Сначала создаем экземпляр ProjectTask
|
||||||
|
// передаем в него Task{ID: taskId}
|
||||||
|
pjTask := &ProjectTask{Task: Task{ID: taskId}}
|
||||||
|
tx := r.db.Find(&pjTask) // ищем задачу
|
||||||
|
if tx.Error != nil {
|
||||||
|
return tx.Error
|
||||||
|
}
|
||||||
|
pjTask.IsDone = true // обновляем поле IsDone
|
||||||
|
r.db.Save(&pjTask) // сохраняем обновленную задачу
|
||||||
|
|
||||||
|
// проверяем обновилась ли задача
|
||||||
|
rowsAffected := tx.RowsAffected
|
||||||
|
if rowsAffected == 0 {
|
||||||
|
return ErrUpdateFailed
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
21
part_6/6.2/golang/todo_gorm/go.mod
Normal file
21
part_6/6.2/golang/todo_gorm/go.mod
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
module golang/todo
|
||||||
|
|
||||||
|
go 1.24
|
||||||
|
|
||||||
|
require github.com/dixonwille/wmenu v4.0.2+incompatible
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/daviddengcn/go-colortext v1.0.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24
|
||||||
|
golang.org/x/sys v0.6.0 // indirect
|
||||||
|
gopkg.in/dixonwille/wlog.v2 v2.0.0 // indirect
|
||||||
|
gorm.io/driver/sqlite v1.5.7
|
||||||
|
gorm.io/gorm v1.25.12
|
||||||
|
)
|
||||||
26
part_6/6.2/golang/todo_gorm/go.sum
Normal file
26
part_6/6.2/golang/todo_gorm/go.sum
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
github.com/daviddengcn/go-colortext v1.0.0 h1:ANqDyC0ys6qCSvuEK7l3g5RaehL/Xck9EX8ATG8oKsE=
|
||||||
|
github.com/daviddengcn/go-colortext v1.0.0/go.mod h1:zDqEI5NVUop5QPpVJUxE9UO10hRnmkD5G4Pmri9+m4c=
|
||||||
|
github.com/dixonwille/wmenu v4.0.2+incompatible h1:lxrPJsx9LpdUFD5T+dOfl6gPKLbBmiAtEdACLT1I2/w=
|
||||||
|
github.com/dixonwille/wmenu v4.0.2+incompatible/go.mod h1:DnajdZEKFQksxBctWekpWaQXQrDUHRBco6b8MyZnR1s=
|
||||||
|
github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho=
|
||||||
|
github.com/golangplus/bytes v1.0.0/go.mod h1:AdRaCFwmc/00ZzELMWb01soso6W1R/++O1XL80yAn+A=
|
||||||
|
github.com/golangplus/fmt v1.0.0/go.mod h1:zpM0OfbMCjPtd2qkTD/jX2MgiFCqklhSUFyDW44gVQE=
|
||||||
|
github.com/golangplus/testing v1.0.0/go.mod h1:ZDreixUV3YzhoVraIDyOzHrr76p6NUh6k/pPg/Q3gYA=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
gopkg.in/dixonwille/wlog.v2 v2.0.0 h1:TbGWtD8ahWVSihKKr+z2Dw7Cv/7IrfN6dwrcrre17pU=
|
||||||
|
gopkg.in/dixonwille/wlog.v2 v2.0.0/go.mod h1:JYQHRnhGPLno/iATOiGkEXoRanJXqdz9Qo6/QwfARUc=
|
||||||
|
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
|
||||||
|
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||||
|
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||||
|
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
16
part_6/6.2/golang/todo_gorm/main.go
Normal file
16
part_6/6.2/golang/todo_gorm/main.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang/todo/db"
|
||||||
|
"golang/todo/menu"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
rep := db.NewSQLiteRepository()
|
||||||
|
defer rep.Close()
|
||||||
|
for {
|
||||||
|
menu.CreateMenu(rep)
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
90
part_6/6.2/golang/todo_gorm/menu/menu.go
Normal file
90
part_6/6.2/golang/todo_gorm/menu/menu.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package menu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"golang/todo/db"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/dixonwille/wmenu"
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateMenu(rep *db.SQLiteRepository) {
|
||||||
|
menu := wmenu.NewMenu("What would you like to do?")
|
||||||
|
|
||||||
|
menu.Action(func(opts []wmenu.Opt) error { handleFunc(rep, opts); return nil })
|
||||||
|
|
||||||
|
menu.Option("Add a new Project", 0, false, nil)
|
||||||
|
menu.Option("Delete a Project by ID", 1, false, nil)
|
||||||
|
menu.Option("Get all Projects", 2, false, nil)
|
||||||
|
menu.Option("Add a Task", 3, false, nil)
|
||||||
|
menu.Option("Get all Tasks", 4, true, nil) // выбор по умолчанию
|
||||||
|
menu.Option("Get all Project tasks", 5, false, nil)
|
||||||
|
menu.Option("Done a Task by ID", 6, false, nil)
|
||||||
|
menu.Option("Delete a Task by ID", 7, false, nil)
|
||||||
|
menu.Option("Quit Application", 8, false, nil)
|
||||||
|
menuerr := menu.Run()
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("---------------------------------")
|
||||||
|
if menuerr != nil {
|
||||||
|
log.Fatal(menuerr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleFunc(rep *db.SQLiteRepository, opts []wmenu.Opt) {
|
||||||
|
switch opts[0].Value {
|
||||||
|
case 0:
|
||||||
|
fmt.Println("Adding a new Project")
|
||||||
|
addProject(rep)
|
||||||
|
case 1:
|
||||||
|
fmt.Println("Deleting a Project by ID")
|
||||||
|
deleteProjectByID(rep)
|
||||||
|
case 2:
|
||||||
|
fmt.Println("Getting all Projects")
|
||||||
|
getAllProjects(rep)
|
||||||
|
case 3:
|
||||||
|
fmt.Println("Adding a new Task")
|
||||||
|
addTask(rep)
|
||||||
|
case 4:
|
||||||
|
fmt.Println("Getting all Tasks")
|
||||||
|
getAllTasks(rep)
|
||||||
|
case 5:
|
||||||
|
fmt.Println("Getting all Project tasks by ProjectID")
|
||||||
|
getAllProjectTasks(rep)
|
||||||
|
case 6:
|
||||||
|
fmt.Println("Doing a Task by ID")
|
||||||
|
doneTask(rep)
|
||||||
|
case 7:
|
||||||
|
fmt.Println("Deleting a Task by ID")
|
||||||
|
deleteTaskByID(rep)
|
||||||
|
case 8:
|
||||||
|
fmt.Println("See you later!!!")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printNotValidData() {
|
||||||
|
fmt.Println("Data is not valid!!!")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getIntValueFromStd(reader *bufio.Reader) (int, error) {
|
||||||
|
tempID, _, _ := reader.ReadLine()
|
||||||
|
idStr := strings.TrimSuffix(string(tempID), "\n")
|
||||||
|
idProj, err := strconv.Atoi(idStr)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return idProj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStringValueFromStd(reader *bufio.Reader) (string, error) {
|
||||||
|
data, err := reader.ReadString('\n')
|
||||||
|
data = strings.TrimSuffix(data, "\r\n")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
65
part_6/6.2/golang/todo_gorm/menu/project_handlers.go
Normal file
65
part_6/6.2/golang/todo_gorm/menu/project_handlers.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package menu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"golang/todo/db"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addProject(rep *db.SQLiteRepository) {
|
||||||
|
project := db.Project{}
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
|
fmt.Print("Input project name: ")
|
||||||
|
name, _ := getStringValueFromStd(reader)
|
||||||
|
|
||||||
|
fmt.Print("Input description project: ")
|
||||||
|
desc, _ := getStringValueFromStd(reader)
|
||||||
|
|
||||||
|
project.Name = name
|
||||||
|
project.Description = desc
|
||||||
|
if project.Name != "" && project.Description != "" {
|
||||||
|
project, err := rep.AddProject(project)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("\nAdded project: %+v\n", *project)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printNotValidData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteProjectByID(rep *db.SQLiteRepository) {
|
||||||
|
fmt.Print("Input ID for deleting project: ")
|
||||||
|
id, err := getIntValueFromStd(bufio.NewReader(os.Stdin))
|
||||||
|
if err != nil {
|
||||||
|
printNotValidData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rep.DeleteProject(id)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Project deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAllProjects(rep *db.SQLiteRepository) {
|
||||||
|
progects, err := rep.GetAllProjects()
|
||||||
|
if err != nil {
|
||||||
|
printNotValidData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(progects) == 0 {
|
||||||
|
fmt.Println("You don't have any project")
|
||||||
|
} else {
|
||||||
|
fmt.Println("You current projects:")
|
||||||
|
for _, it := range progects {
|
||||||
|
fmt.Printf("ProjectID: %v || Name: %v || Desc: %v\n",
|
||||||
|
it.ID, it.Name, it.Description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
122
part_6/6.2/golang/todo_gorm/menu/task_handlers.go
Normal file
122
part_6/6.2/golang/todo_gorm/menu/task_handlers.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package menu
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"golang/todo/db"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addTask(rep *db.SQLiteRepository) {
|
||||||
|
task := db.Task{}
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
|
fmt.Print("Input project ID: ")
|
||||||
|
projectID, err := getIntValueFromStd(reader)
|
||||||
|
if err != nil {
|
||||||
|
printNotValidData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Print("Input task name: ")
|
||||||
|
name, _ := getStringValueFromStd(reader)
|
||||||
|
|
||||||
|
fmt.Print("Input description task: ")
|
||||||
|
desc, _ := getStringValueFromStd(reader)
|
||||||
|
|
||||||
|
fmt.Print("Input priority task: ")
|
||||||
|
priority, err := getIntValueFromStd(reader)
|
||||||
|
if err != nil {
|
||||||
|
printNotValidData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
task.Name = name
|
||||||
|
task.Description = desc
|
||||||
|
task.Priority = uint8(priority)
|
||||||
|
if task.Name != "" && task.Description != "" {
|
||||||
|
task, err := rep.AddTask(task, projectID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("\nAdded task: %+v\n", *task)
|
||||||
|
} else {
|
||||||
|
printNotValidData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteTaskByID(rep *db.SQLiteRepository) {
|
||||||
|
fmt.Print("Input ID for deleting task: ")
|
||||||
|
id, err := getIntValueFromStd(bufio.NewReader(os.Stdin))
|
||||||
|
if err != nil {
|
||||||
|
printNotValidData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rep.DeleteTask(id)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Task deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAllTasks(rep *db.SQLiteRepository) {
|
||||||
|
tasks, err := rep.GetAllTasks()
|
||||||
|
if err != nil {
|
||||||
|
printNotValidData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(tasks) == 0 {
|
||||||
|
fmt.Println("You don't have any task")
|
||||||
|
} else {
|
||||||
|
fmt.Println("You current tasks: ")
|
||||||
|
for _, it := range tasks {
|
||||||
|
fmt.Printf("TaskID: %v || Name: %v || Desc: %v || Priority: %v || IsDone: %v || ProjID: %v\n",
|
||||||
|
it.ID, it.Name, it.Description, it.Priority, it.IsDone, it.ProjectID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAllProjectTasks(rep *db.SQLiteRepository) {
|
||||||
|
fmt.Print("Input ID for project: ")
|
||||||
|
|
||||||
|
id, err := getIntValueFromStd(bufio.NewReader(os.Stdin))
|
||||||
|
if err != nil {
|
||||||
|
printNotValidData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks, err := rep.GetProjectTasks(id)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tasks) == 0 {
|
||||||
|
fmt.Println("You don't have any task")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Project with ID = %d have next tasks:\n", id)
|
||||||
|
for _, it := range tasks {
|
||||||
|
fmt.Printf("TaskID: %v || Name: %v || Desc: %v || Priority: %v || IsDone: %v\n",
|
||||||
|
it.ID, it.Name, it.Description, it.Priority, it.IsDone)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func doneTask(rep *db.SQLiteRepository) {
|
||||||
|
fmt.Print("Input task ID: ")
|
||||||
|
|
||||||
|
id, err := getIntValueFromStd(bufio.NewReader(os.Stdin))
|
||||||
|
if err != nil {
|
||||||
|
printNotValidData()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = rep.TaskDone(id)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Congratulations! Task done!")
|
||||||
|
}
|
||||||
BIN
part_6/6.2/golang/todo_gorm/todo.db
Normal file
BIN
part_6/6.2/golang/todo_gorm/todo.db
Normal file
Binary file not shown.
14
part_6/6.3/golang/todo_transaction/go.mod
Normal file
14
part_6/6.3/golang/todo_transaction/go.mod
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
module golang/todo
|
||||||
|
|
||||||
|
go 1.24
|
||||||
|
|
||||||
|
require gorm.io/gorm v1.25.12
|
||||||
|
|
||||||
|
require github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
gorm.io/driver/sqlite v1.5.7
|
||||||
|
)
|
||||||
12
part_6/6.3/golang/todo_transaction/go.sum
Normal file
12
part_6/6.3/golang/todo_transaction/go.sum
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
|
||||||
|
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||||
|
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||||
|
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||||
243
part_6/6.3/golang/todo_transaction/main.go
Normal file
243
part_6/6.3/golang/todo_transaction/main.go
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
const dbName = "todo.db"
|
||||||
|
|
||||||
|
var ErrNotExists = errors.New("row not exists")
|
||||||
|
|
||||||
|
type Project struct {
|
||||||
|
ID int `gorm:"primary_key;autoIncrement:true;not null"`
|
||||||
|
Name string `gorm:"unique;not null"`
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProjectTask struct {
|
||||||
|
ID int `gorm:"primary_key;autoIncrement;not null"`
|
||||||
|
Name string `gorm:"not null"`
|
||||||
|
Description string `gorm:"not null"`
|
||||||
|
Priority uint8 `gorm:"not null"`
|
||||||
|
IsDone bool `gorm:"not null"`
|
||||||
|
ProjectID int `gorm:"not null"`
|
||||||
|
Project *Project `gorm:"foreignKey:ProjectID;references:ID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectionToBD(pathToDB string) (db *gorm.DB, err error) {
|
||||||
|
if _, err := os.Stat(dbName); os.IsNotExist(err) {
|
||||||
|
db, err = gorm.Open(sqlite.Open(dbName), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fmt.Println("DB isn't exist")
|
||||||
|
db.AutoMigrate(&Project{}, &ProjectTask{})
|
||||||
|
putDefaultValuesToDB(db)
|
||||||
|
} else {
|
||||||
|
db, err = gorm.Open(sqlite.Open(dbName), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fmt.Println("DB already exists")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func putDefaultValuesToDB(db *gorm.DB) {
|
||||||
|
db.Transaction(func(tx *gorm.DB) error { // начало транзакции
|
||||||
|
firstProject := Project{
|
||||||
|
Name: "Go",
|
||||||
|
Description: "Roadmap for learning Go",
|
||||||
|
}
|
||||||
|
secondProject := Project{
|
||||||
|
Name: "One Year",
|
||||||
|
Description: "Tasks for the year",
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Create(&firstProject)
|
||||||
|
|
||||||
|
if err := tx.Create(&secondProject).Error; err != nil { //проверяем на наличие ошибок при записи
|
||||||
|
return err // вызываем отмену транзакции
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Create(&ProjectTask{
|
||||||
|
Name: "Variable",
|
||||||
|
Description: "Learning Go build-in variables",
|
||||||
|
Priority: 1,
|
||||||
|
Project: &firstProject,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := tx.Create(&ProjectTask{ //проверяем на наличие ошибок при записи
|
||||||
|
Name: "Struct",
|
||||||
|
Description: "Learning use struct in OOP code",
|
||||||
|
Priority: 3,
|
||||||
|
Project: &firstProject,
|
||||||
|
}).Error; err != nil {
|
||||||
|
return err // вызываем отмену транзакции
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Create(&ProjectTask{
|
||||||
|
Name: "Goroutine",
|
||||||
|
Description: "Learning concurrent programming",
|
||||||
|
Priority: 5,
|
||||||
|
Project: &firstProject,
|
||||||
|
})
|
||||||
|
tx.Create(&ProjectTask{
|
||||||
|
Name: "DataBase",
|
||||||
|
Description: "How write app with db",
|
||||||
|
Priority: 1,
|
||||||
|
Project: &firstProject,
|
||||||
|
})
|
||||||
|
tx.Create(&ProjectTask{
|
||||||
|
Name: "PhD",
|
||||||
|
Description: "Ph.D. in Technical Sciences",
|
||||||
|
Priority: 5,
|
||||||
|
Project: &secondProject,
|
||||||
|
})
|
||||||
|
tx.Create(&ProjectTask{
|
||||||
|
Name: "Losing weight",
|
||||||
|
Description: "Exercise and eat less chocolate",
|
||||||
|
Priority: 2,
|
||||||
|
Project: &secondProject,
|
||||||
|
})
|
||||||
|
tx.Create(&ProjectTask{
|
||||||
|
Name: "Пафос и превозмогание",
|
||||||
|
Description: "10к подписчиков на канале",
|
||||||
|
Priority: 2,
|
||||||
|
Project: &secondProject,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllProjects(db *gorm.DB) ([]Project, error) {
|
||||||
|
var projects []Project
|
||||||
|
tx := db.Find(&projects)
|
||||||
|
if tx.Error != nil {
|
||||||
|
return nil, tx.Error
|
||||||
|
}
|
||||||
|
if tx.RowsAffected == 0 {
|
||||||
|
return nil, ErrNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
return projects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllTasks(db *gorm.DB) (tasks []ProjectTask, err error) {
|
||||||
|
tx := db.Find(&tasks)
|
||||||
|
if tx.Error != nil {
|
||||||
|
return nil, tx.Error
|
||||||
|
}
|
||||||
|
if tx.RowsAffected == 0 {
|
||||||
|
return nil, ErrNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func printAllTasks(db *gorm.DB) {
|
||||||
|
tasks, err := GetAllTasks(db)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("*********Tasks*********")
|
||||||
|
for _, it := range tasks {
|
||||||
|
fmt.Printf("TaskID: %v || Name: %v || Priority: %v || IsDone: %v || ProjID: %v\n",
|
||||||
|
it.ID, it.Name, it.Priority, it.IsDone, it.ProjectID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printAllProjects(db *gorm.DB) {
|
||||||
|
progects, err := GetAllProjects(db)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("*********Projects*********")
|
||||||
|
for _, it := range progects {
|
||||||
|
fmt.Printf("ProjectID: %v || Name: %v || Desc: %v\n",
|
||||||
|
it.ID, it.Name, it.Description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func printAllProjectAndTask(db *gorm.DB) {
|
||||||
|
printAllProjects(db)
|
||||||
|
printAllTasks(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllProjectTasks(db *gorm.DB, projectID int) (tasks []ProjectTask, err error) {
|
||||||
|
tx := db.Where("project_id", projectID).Find(&tasks)
|
||||||
|
if tx.Error != nil {
|
||||||
|
return nil, tx.Error
|
||||||
|
}
|
||||||
|
if tx.RowsAffected == 0 {
|
||||||
|
return nil, ErrNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func printAllProjectTasks(db *gorm.DB, projectID int) {
|
||||||
|
tasks, err := GetAllProjectTasks(db, projectID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("*********Tasks*********")
|
||||||
|
for _, it := range tasks {
|
||||||
|
fmt.Printf("TaskID: %v || Name: %v || Priority: %v || IsDone: %v || ProjID: %v\n",
|
||||||
|
it.ID, it.Name, it.Priority, it.IsDone, it.ProjectID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
db, _ := gorm.Open(sqlite.Open(dbName), &gorm.Config{DryRun: true})
|
||||||
|
|
||||||
|
var tasks []ProjectTask
|
||||||
|
stmt := db.Where("project_id", 1).Find(&tasks).Statement
|
||||||
|
fmt.Println(stmt.SQL.String())
|
||||||
|
|
||||||
|
stmt = db.Create(&ProjectTask{
|
||||||
|
Name: "Пафос и превозмогание",
|
||||||
|
Description: "10к подписчиков на канале",
|
||||||
|
Priority: 2,
|
||||||
|
ProjectID: 2,
|
||||||
|
}).Statement
|
||||||
|
fmt.Println(stmt.SQL.String())
|
||||||
|
|
||||||
|
firstProject := Project{
|
||||||
|
Name: "Go",
|
||||||
|
Description: "Roadmap for learning Go",
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt = db.Create(&firstProject).Statement
|
||||||
|
fmt.Println(stmt.SQL.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateProjects(db *gorm.DB) error {
|
||||||
|
tx := db.Begin()
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := tx.Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Create(&Project{Name: "Oo"}).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Create(&Project{Name: "^_^"}).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit().Error
|
||||||
|
}
|
||||||
BIN
part_6/6.3/golang/todo_transaction/todo.db
Normal file
BIN
part_6/6.3/golang/todo_transaction/todo.db
Normal file
Binary file not shown.
14
part_6/tic_tac_toe/.vscode/launch.json
vendored
Normal file
14
part_6/tic_tac_toe/.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Launch Package",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${fileDirname}", // <- ставим запятую
|
||||||
|
"console": "integratedTerminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
113
part_6/tic_tac_toe/game/board.go
Normal file
113
part_6/tic_tac_toe/game/board.go
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
BoardDefaultSize int = 3
|
||||||
|
BoardMinSize int = 3
|
||||||
|
BoardMaxSize int = 9
|
||||||
|
)
|
||||||
|
|
||||||
|
type Board struct {
|
||||||
|
Board [][]BoardField `json:"board"`
|
||||||
|
Size int `json:"size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBoard(size int) *Board {
|
||||||
|
board := make([][]BoardField, size)
|
||||||
|
for i := range board {
|
||||||
|
board[i] = make([]BoardField, size)
|
||||||
|
}
|
||||||
|
return &Board{Board: board, Size: size}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отображение игрового поля
|
||||||
|
func (b *Board) printBoard() {
|
||||||
|
fmt.Print(" ")
|
||||||
|
for i := range b.Size {
|
||||||
|
fmt.Printf("%d ", i+1)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
for i := range b.Size {
|
||||||
|
fmt.Printf("%d ", i+1)
|
||||||
|
for j := range b.Size {
|
||||||
|
switch b.Board[i][j] {
|
||||||
|
case empty:
|
||||||
|
fmt.Print(". ")
|
||||||
|
case cross:
|
||||||
|
fmt.Print("X ")
|
||||||
|
case nought:
|
||||||
|
fmt.Print("O ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка возможности и выполнения хода
|
||||||
|
func (b *Board) makeMove(x, y int) bool {
|
||||||
|
return b.Board[x][y] == empty
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Board) setSymbol(x, y int, player BoardField) bool {
|
||||||
|
if b.makeMove(x, y) {
|
||||||
|
b.Board[x][y] = player
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка выигрыша
|
||||||
|
func (b *Board) checkWin(player BoardField) bool {
|
||||||
|
// Проверка строк и столбцов
|
||||||
|
for i := range b.Size {
|
||||||
|
rowWin, colWin := true, true
|
||||||
|
for j := range b.Size {
|
||||||
|
if b.Board[i][j] != player {
|
||||||
|
rowWin = false
|
||||||
|
}
|
||||||
|
if b.Board[j][i] != player {
|
||||||
|
colWin = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rowWin || colWin {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Главная диагональ
|
||||||
|
mainDiag := true
|
||||||
|
for i := range b.Size {
|
||||||
|
if b.Board[i][i] != player {
|
||||||
|
mainDiag = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if mainDiag {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Побочная диагональ
|
||||||
|
antiDiag := true
|
||||||
|
for i := range b.Size {
|
||||||
|
if b.Board[i][b.Size-i-1] != player {
|
||||||
|
antiDiag = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return antiDiag
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка на ничью
|
||||||
|
func (b *Board) checkDraw() bool {
|
||||||
|
for i := range b.Size {
|
||||||
|
for j := range b.Size {
|
||||||
|
if b.Board[i][j] == empty {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
10
part_6/tic_tac_toe/game/board_cell_type.go
Normal file
10
part_6/tic_tac_toe/game/board_cell_type.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
type BoardField int
|
||||||
|
|
||||||
|
// фигуры в клетке поля
|
||||||
|
const (
|
||||||
|
empty BoardField = iota
|
||||||
|
cross
|
||||||
|
nought
|
||||||
|
)
|
||||||
122
part_6/tic_tac_toe/game/game.go
Normal file
122
part_6/tic_tac_toe/game/game.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Game struct {
|
||||||
|
Board *Board `json:"board"`
|
||||||
|
Player *Player `json:"player"`
|
||||||
|
Reader *bufio.Reader `json:"-"`
|
||||||
|
State GameState `json:"state"`
|
||||||
|
Saver IGameSaver `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGame(board Board, player Player,
|
||||||
|
reader *bufio.Reader, saver IGameSaver) *Game {
|
||||||
|
return &Game{
|
||||||
|
Board: &board,
|
||||||
|
Player: &player,
|
||||||
|
Reader: reader,
|
||||||
|
State: playing,
|
||||||
|
Saver: saver,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) updateState() {
|
||||||
|
if g.Board.checkWin(g.Player.Figure) {
|
||||||
|
if g.Player.Figure == cross {
|
||||||
|
g.State = crossWin
|
||||||
|
} else {
|
||||||
|
g.State = noughtWin
|
||||||
|
}
|
||||||
|
} else if g.Board.checkDraw() {
|
||||||
|
g.State = draw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) saveCheck(input string) bool {
|
||||||
|
if input == "save" {
|
||||||
|
fmt.Println("Enter file name: ")
|
||||||
|
fileName, err := g.Reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Invalid input. Please try again.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fileName = strings.TrimSpace(fileName)
|
||||||
|
err = g.Saver.SaveGame(fileName, g)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error saving game.")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
fmt.Println("Game saved successfully!!!")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Игровой цикл
|
||||||
|
func (g *Game) Play() {
|
||||||
|
for g.State == playing {
|
||||||
|
g.Board.printBoard()
|
||||||
|
fmt.Printf(
|
||||||
|
"%s's turn. Enter row and column (e.g. 1 2): ",
|
||||||
|
g.Player.getSymbol())
|
||||||
|
|
||||||
|
input, err := g.Reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Invalid input. Please try again.")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
if input == "q" {
|
||||||
|
g.State = quit
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.saveCheck(input) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Fields(input)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
fmt.Println("Invalid input. Please try again.")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
row, err1 := strconv.Atoi(parts[0])
|
||||||
|
col, err2 := strconv.Atoi(parts[1])
|
||||||
|
if err1 != nil || err2 != nil ||
|
||||||
|
row < 1 || col < 1 || row > g.Board.Size ||
|
||||||
|
col > g.Board.Size {
|
||||||
|
fmt.Println("Invalid input. Please try again.")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if g.Board.setSymbol(row-1, col-1, g.Player.Figure) {
|
||||||
|
g.updateState()
|
||||||
|
g.Player.switchPlayer()
|
||||||
|
|
||||||
|
} else {
|
||||||
|
fmt.Println("This cell is already occupied!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Board.printBoard()
|
||||||
|
|
||||||
|
if g.State == crossWin {
|
||||||
|
fmt.Println("X wins!")
|
||||||
|
} else if g.State == noughtWin {
|
||||||
|
fmt.Println("O wins!")
|
||||||
|
} else if g.State == draw {
|
||||||
|
fmt.Println("It's a draw!")
|
||||||
|
} else {
|
||||||
|
fmt.Println("Game over!")
|
||||||
|
}
|
||||||
|
}
|
||||||
12
part_6/tic_tac_toe/game/game_state.go
Normal file
12
part_6/tic_tac_toe/game/game_state.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
type GameState int
|
||||||
|
|
||||||
|
// состояние игрового процесса
|
||||||
|
const (
|
||||||
|
playing GameState = iota
|
||||||
|
draw
|
||||||
|
crossWin
|
||||||
|
noughtWin
|
||||||
|
quit
|
||||||
|
)
|
||||||
24
part_6/tic_tac_toe/game/player.go
Normal file
24
part_6/tic_tac_toe/game/player.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package game
|
||||||
|
|
||||||
|
type Player struct {
|
||||||
|
Figure BoardField `json:"figure"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPlayer() *Player {
|
||||||
|
return &Player{Figure: cross}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Player) switchPlayer() {
|
||||||
|
if p.Figure == cross {
|
||||||
|
p.Figure = nought
|
||||||
|
} else {
|
||||||
|
p.Figure = cross
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Player) getSymbol() string {
|
||||||
|
if p.Figure == cross {
|
||||||
|
return "X"
|
||||||
|
}
|
||||||
|
return "O"
|
||||||
|
}
|
||||||
3
part_6/tic_tac_toe/go.mod
Normal file
3
part_6/tic_tac_toe/go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module tic-tac-toe
|
||||||
|
|
||||||
|
go 1.24.0
|
||||||
1
part_6/tic_tac_toe/myGame.json
Normal file
1
part_6/tic_tac_toe/myGame.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"board":{"board":[[0,2,0,0],[0,0,1,0],[0,0,0,0],[0,0,1,0]],"size":4},"player":{"figure":2},"state":0}
|
||||||
Reference in New Issue
Block a user