From 0029a1d728c78d57953ec9158497e20fcf098076 Mon Sep 17 00:00:00 2001 From: Stanislav Chernyshev Date: Mon, 16 Jun 2025 11:31:09 +0300 Subject: [PATCH] add new project step and small fix --- .../.vscode/launch.json | 0 .../game/board.go | 0 .../game/board_cell_type.go | 0 .../game/game.go | 0 .../game/game_state.go | 0 part_5/tic_tac_toe_v3/game/i_game_storage.go | 14 + .../tic_tac_toe_v3/game/json_game_storage.go | 50 +++ .../game/player.go | 0 part_5/{tic_tac_toe => tic_tac_toe_v3}/go.mod | 0 part_5/tic_tac_toe_v3/main.go | 79 +++++ .../myGame.json | 0 part_5/tic_tac_toe_v4/.vscode/launch.json | 14 + part_5/tic_tac_toe_v4/board/board.go | 113 +++++++ .../tic_tac_toe_v4/board/board_cell_type.go | 10 + part_5/tic_tac_toe_v4/game/game_core.go | 79 +++++ part_5/tic_tac_toe_v4/game/game_play.go | 127 +++++++ .../tic_tac_toe_v4/game/game_serialization.go | 82 +++++ part_5/tic_tac_toe_v4/game/game_setup.go | 116 +++++++ part_5/tic_tac_toe_v4/game/game_state.go | 20 ++ part_5/tic_tac_toe_v4/go.mod | 3 + part_5/tic_tac_toe_v4/main.go | 69 ++++ part_5/tic_tac_toe_v4/model/game_snapshot.go | 16 + .../tic_tac_toe_v4/player/computer_player.go | 310 ++++++++++++++++++ part_5/tic_tac_toe_v4/player/i_player.go | 22 ++ part_5/tic_tac_toe_v4/player/player.go | 104 ++++++ part_5/tic_tac_toe_v4/rt.json | 1 + .../tic_tac_toe_v4/storage/i_game_storage.go | 16 + .../storage/json_game_storage.go | 52 +++ part_6/6.1/golang/todo/.vscode/launch.json | 13 + part_6/6.1/golang/todo/db/db.go | 94 ++++++ part_6/6.1/golang/todo/db/db_definition.go | 31 ++ part_6/6.1/golang/todo/db/models.go | 20 ++ part_6/6.1/golang/todo/db/projects_crud.go | 92 ++++++ part_6/6.1/golang/todo/db/tasks_crud.go | 129 ++++++++ part_6/6.1/golang/todo/go.mod | 12 + part_6/6.1/golang/todo/go.sum | 17 + part_6/6.1/golang/todo/main.go | 19 ++ part_6/6.1/golang/todo/menu/menu.go | 101 ++++++ .../6.1/golang/todo/menu/project_handlers.go | 71 ++++ part_6/6.1/golang/todo/menu/task_handlers.go | 136 ++++++++ part_6/6.1/golang/todo/todo.db | Bin 0 -> 20480 bytes part_6/6.2/golang/todo_gorm/db/db.go | 94 ++++++ .../6.2/golang/todo_gorm/db/db_definition.go | 12 + part_6/6.2/golang/todo_gorm/db/models.go | 21 ++ .../6.2/golang/todo_gorm/db/projects_crud.go | 53 +++ part_6/6.2/golang/todo_gorm/db/tasks_crud.go | 95 ++++++ part_6/6.2/golang/todo_gorm/go.mod | 21 ++ part_6/6.2/golang/todo_gorm/go.sum | 26 ++ part_6/6.2/golang/todo_gorm/main.go | 16 + part_6/6.2/golang/todo_gorm/menu/menu.go | 90 +++++ .../golang/todo_gorm/menu/project_handlers.go | 65 ++++ .../golang/todo_gorm/menu/task_handlers.go | 122 +++++++ part_6/6.2/golang/todo_gorm/todo.db | Bin 0 -> 20480 bytes part_6/6.3/golang/todo_transaction/go.mod | 14 + part_6/6.3/golang/todo_transaction/go.sum | 12 + part_6/6.3/golang/todo_transaction/main.go | 243 ++++++++++++++ part_6/6.3/golang/todo_transaction/todo.db | Bin 0 -> 16384 bytes part_6/tic_tac_toe/.vscode/launch.json | 14 + part_6/tic_tac_toe/game/board.go | 113 +++++++ part_6/tic_tac_toe/game/board_cell_type.go | 10 + part_6/tic_tac_toe/game/game.go | 122 +++++++ part_6/tic_tac_toe/game/game_state.go | 12 + .../tic_tac_toe/game/i_game_loader.go | 0 .../tic_tac_toe/game/json_game_loader.go | 0 part_6/tic_tac_toe/game/player.go | 24 ++ part_6/tic_tac_toe/go.mod | 3 + {part_5 => part_6}/tic_tac_toe/main.go | 0 part_6/tic_tac_toe/myGame.json | 1 + 68 files changed, 3215 insertions(+) rename part_5/{tic_tac_toe => tic_tac_toe_v3}/.vscode/launch.json (100%) rename part_5/{tic_tac_toe => tic_tac_toe_v3}/game/board.go (100%) rename part_5/{tic_tac_toe => tic_tac_toe_v3}/game/board_cell_type.go (100%) rename part_5/{tic_tac_toe => tic_tac_toe_v3}/game/game.go (100%) rename part_5/{tic_tac_toe => tic_tac_toe_v3}/game/game_state.go (100%) create mode 100644 part_5/tic_tac_toe_v3/game/i_game_storage.go create mode 100644 part_5/tic_tac_toe_v3/game/json_game_storage.go rename part_5/{tic_tac_toe => tic_tac_toe_v3}/game/player.go (100%) rename part_5/{tic_tac_toe => tic_tac_toe_v3}/go.mod (100%) create mode 100644 part_5/tic_tac_toe_v3/main.go rename part_5/{tic_tac_toe => tic_tac_toe_v3}/myGame.json (100%) create mode 100644 part_5/tic_tac_toe_v4/.vscode/launch.json create mode 100644 part_5/tic_tac_toe_v4/board/board.go create mode 100644 part_5/tic_tac_toe_v4/board/board_cell_type.go create mode 100644 part_5/tic_tac_toe_v4/game/game_core.go create mode 100644 part_5/tic_tac_toe_v4/game/game_play.go create mode 100644 part_5/tic_tac_toe_v4/game/game_serialization.go create mode 100644 part_5/tic_tac_toe_v4/game/game_setup.go create mode 100644 part_5/tic_tac_toe_v4/game/game_state.go create mode 100644 part_5/tic_tac_toe_v4/go.mod create mode 100644 part_5/tic_tac_toe_v4/main.go create mode 100644 part_5/tic_tac_toe_v4/model/game_snapshot.go create mode 100644 part_5/tic_tac_toe_v4/player/computer_player.go create mode 100644 part_5/tic_tac_toe_v4/player/i_player.go create mode 100644 part_5/tic_tac_toe_v4/player/player.go create mode 100644 part_5/tic_tac_toe_v4/rt.json create mode 100644 part_5/tic_tac_toe_v4/storage/i_game_storage.go create mode 100644 part_5/tic_tac_toe_v4/storage/json_game_storage.go create mode 100644 part_6/6.1/golang/todo/.vscode/launch.json create mode 100644 part_6/6.1/golang/todo/db/db.go create mode 100644 part_6/6.1/golang/todo/db/db_definition.go create mode 100644 part_6/6.1/golang/todo/db/models.go create mode 100644 part_6/6.1/golang/todo/db/projects_crud.go create mode 100644 part_6/6.1/golang/todo/db/tasks_crud.go create mode 100644 part_6/6.1/golang/todo/go.mod create mode 100644 part_6/6.1/golang/todo/go.sum create mode 100644 part_6/6.1/golang/todo/main.go create mode 100644 part_6/6.1/golang/todo/menu/menu.go create mode 100644 part_6/6.1/golang/todo/menu/project_handlers.go create mode 100644 part_6/6.1/golang/todo/menu/task_handlers.go create mode 100644 part_6/6.1/golang/todo/todo.db create mode 100644 part_6/6.2/golang/todo_gorm/db/db.go create mode 100644 part_6/6.2/golang/todo_gorm/db/db_definition.go create mode 100644 part_6/6.2/golang/todo_gorm/db/models.go create mode 100644 part_6/6.2/golang/todo_gorm/db/projects_crud.go create mode 100644 part_6/6.2/golang/todo_gorm/db/tasks_crud.go create mode 100644 part_6/6.2/golang/todo_gorm/go.mod create mode 100644 part_6/6.2/golang/todo_gorm/go.sum create mode 100644 part_6/6.2/golang/todo_gorm/main.go create mode 100644 part_6/6.2/golang/todo_gorm/menu/menu.go create mode 100644 part_6/6.2/golang/todo_gorm/menu/project_handlers.go create mode 100644 part_6/6.2/golang/todo_gorm/menu/task_handlers.go create mode 100644 part_6/6.2/golang/todo_gorm/todo.db create mode 100644 part_6/6.3/golang/todo_transaction/go.mod create mode 100644 part_6/6.3/golang/todo_transaction/go.sum create mode 100644 part_6/6.3/golang/todo_transaction/main.go create mode 100644 part_6/6.3/golang/todo_transaction/todo.db create mode 100644 part_6/tic_tac_toe/.vscode/launch.json create mode 100644 part_6/tic_tac_toe/game/board.go create mode 100644 part_6/tic_tac_toe/game/board_cell_type.go create mode 100644 part_6/tic_tac_toe/game/game.go create mode 100644 part_6/tic_tac_toe/game/game_state.go rename {part_5 => part_6}/tic_tac_toe/game/i_game_loader.go (100%) rename {part_5 => part_6}/tic_tac_toe/game/json_game_loader.go (100%) create mode 100644 part_6/tic_tac_toe/game/player.go create mode 100644 part_6/tic_tac_toe/go.mod rename {part_5 => part_6}/tic_tac_toe/main.go (100%) create mode 100644 part_6/tic_tac_toe/myGame.json diff --git a/part_5/tic_tac_toe/.vscode/launch.json b/part_5/tic_tac_toe_v3/.vscode/launch.json similarity index 100% rename from part_5/tic_tac_toe/.vscode/launch.json rename to part_5/tic_tac_toe_v3/.vscode/launch.json diff --git a/part_5/tic_tac_toe/game/board.go b/part_5/tic_tac_toe_v3/game/board.go similarity index 100% rename from part_5/tic_tac_toe/game/board.go rename to part_5/tic_tac_toe_v3/game/board.go diff --git a/part_5/tic_tac_toe/game/board_cell_type.go b/part_5/tic_tac_toe_v3/game/board_cell_type.go similarity index 100% rename from part_5/tic_tac_toe/game/board_cell_type.go rename to part_5/tic_tac_toe_v3/game/board_cell_type.go diff --git a/part_5/tic_tac_toe/game/game.go b/part_5/tic_tac_toe_v3/game/game.go similarity index 100% rename from part_5/tic_tac_toe/game/game.go rename to part_5/tic_tac_toe_v3/game/game.go diff --git a/part_5/tic_tac_toe/game/game_state.go b/part_5/tic_tac_toe_v3/game/game_state.go similarity index 100% rename from part_5/tic_tac_toe/game/game_state.go rename to part_5/tic_tac_toe_v3/game/game_state.go diff --git a/part_5/tic_tac_toe_v3/game/i_game_storage.go b/part_5/tic_tac_toe_v3/game/i_game_storage.go new file mode 100644 index 0000000..964e2f2 --- /dev/null +++ b/part_5/tic_tac_toe_v3/game/i_game_storage.go @@ -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 +} diff --git a/part_5/tic_tac_toe_v3/game/json_game_storage.go b/part_5/tic_tac_toe_v3/game/json_game_storage.go new file mode 100644 index 0000000..8553c4b --- /dev/null +++ b/part_5/tic_tac_toe_v3/game/json_game_storage.go @@ -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 +} diff --git a/part_5/tic_tac_toe/game/player.go b/part_5/tic_tac_toe_v3/game/player.go similarity index 100% rename from part_5/tic_tac_toe/game/player.go rename to part_5/tic_tac_toe_v3/game/player.go diff --git a/part_5/tic_tac_toe/go.mod b/part_5/tic_tac_toe_v3/go.mod similarity index 100% rename from part_5/tic_tac_toe/go.mod rename to part_5/tic_tac_toe_v3/go.mod diff --git a/part_5/tic_tac_toe_v3/main.go b/part_5/tic_tac_toe_v3/main.go new file mode 100644 index 0000000..17d7cd4 --- /dev/null +++ b/part_5/tic_tac_toe_v3/main.go @@ -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 + } + } +} diff --git a/part_5/tic_tac_toe/myGame.json b/part_5/tic_tac_toe_v3/myGame.json similarity index 100% rename from part_5/tic_tac_toe/myGame.json rename to part_5/tic_tac_toe_v3/myGame.json diff --git a/part_5/tic_tac_toe_v4/.vscode/launch.json b/part_5/tic_tac_toe_v4/.vscode/launch.json new file mode 100644 index 0000000..edd87ae --- /dev/null +++ b/part_5/tic_tac_toe_v4/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + "version": "0.2.0", + "configurations": [ + + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}", // <- ставим запятую + "console": "integratedTerminal" + } + ] +} diff --git a/part_5/tic_tac_toe_v4/board/board.go b/part_5/tic_tac_toe_v4/board/board.go new file mode 100644 index 0000000..f32c64e --- /dev/null +++ b/part_5/tic_tac_toe_v4/board/board.go @@ -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 +} diff --git a/part_5/tic_tac_toe_v4/board/board_cell_type.go b/part_5/tic_tac_toe_v4/board/board_cell_type.go new file mode 100644 index 0000000..1beb472 --- /dev/null +++ b/part_5/tic_tac_toe_v4/board/board_cell_type.go @@ -0,0 +1,10 @@ +package board + +type BoardField int + +// фигуры в клетке поля +const ( + Empty BoardField = iota + Cross + Nought +) diff --git a/part_5/tic_tac_toe_v4/game/game_core.go b/part_5/tic_tac_toe_v4/game/game_core.go new file mode 100644 index 0000000..69bccf1 --- /dev/null +++ b/part_5/tic_tac_toe_v4/game/game_core.go @@ -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 + } +} diff --git a/part_5/tic_tac_toe_v4/game/game_play.go b/part_5/tic_tac_toe_v4/game/game_play.go new file mode 100644 index 0000000..7348934 --- /dev/null +++ b/part_5/tic_tac_toe_v4/game/game_play.go @@ -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 +} diff --git a/part_5/tic_tac_toe_v4/game/game_serialization.go b/part_5/tic_tac_toe_v4/game/game_serialization.go new file mode 100644 index 0000000..d5934c0 --- /dev/null +++ b/part_5/tic_tac_toe_v4/game/game_serialization.go @@ -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 + } +} diff --git a/part_5/tic_tac_toe_v4/game/game_setup.go b/part_5/tic_tac_toe_v4/game/game_setup.go new file mode 100644 index 0000000..c7be777 --- /dev/null +++ b/part_5/tic_tac_toe_v4/game/game_setup.go @@ -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!") + } + } +} diff --git a/part_5/tic_tac_toe_v4/game/game_state.go b/part_5/tic_tac_toe_v4/game/game_state.go new file mode 100644 index 0000000..2f0b218 --- /dev/null +++ b/part_5/tic_tac_toe_v4/game/game_state.go @@ -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 +) diff --git a/part_5/tic_tac_toe_v4/go.mod b/part_5/tic_tac_toe_v4/go.mod new file mode 100644 index 0000000..ced943a --- /dev/null +++ b/part_5/tic_tac_toe_v4/go.mod @@ -0,0 +1,3 @@ +module tic-tac-toe + +go 1.24.0 diff --git a/part_5/tic_tac_toe_v4/main.go b/part_5/tic_tac_toe_v4/main.go new file mode 100644 index 0000000..6407fc1 --- /dev/null +++ b/part_5/tic_tac_toe_v4/main.go @@ -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.") + } + } +} diff --git a/part_5/tic_tac_toe_v4/model/game_snapshot.go b/part_5/tic_tac_toe_v4/model/game_snapshot.go new file mode 100644 index 0000000..a3bc1c1 --- /dev/null +++ b/part_5/tic_tac_toe_v4/model/game_snapshot.go @@ -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"` +} diff --git a/part_5/tic_tac_toe_v4/player/computer_player.go b/part_5/tic_tac_toe_v4/player/computer_player.go new file mode 100644 index 0000000..120fb5a --- /dev/null +++ b/part_5/tic_tac_toe_v4/player/computer_player.go @@ -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 +} diff --git a/part_5/tic_tac_toe_v4/player/i_player.go b/part_5/tic_tac_toe_v4/player/i_player.go new file mode 100644 index 0000000..2538204 --- /dev/null +++ b/part_5/tic_tac_toe_v4/player/i_player.go @@ -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 +} diff --git a/part_5/tic_tac_toe_v4/player/player.go b/part_5/tic_tac_toe_v4/player/player.go new file mode 100644 index 0000000..248a8f2 --- /dev/null +++ b/part_5/tic_tac_toe_v4/player/player.go @@ -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() +} diff --git a/part_5/tic_tac_toe_v4/rt.json b/part_5/tic_tac_toe_v4/rt.json new file mode 100644 index 0000000..b47e72c --- /dev/null +++ b/part_5/tic_tac_toe_v4/rt.json @@ -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} diff --git a/part_5/tic_tac_toe_v4/storage/i_game_storage.go b/part_5/tic_tac_toe_v4/storage/i_game_storage.go new file mode 100644 index 0000000..c99014a --- /dev/null +++ b/part_5/tic_tac_toe_v4/storage/i_game_storage.go @@ -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 +} diff --git a/part_5/tic_tac_toe_v4/storage/json_game_storage.go b/part_5/tic_tac_toe_v4/storage/json_game_storage.go new file mode 100644 index 0000000..c0e9f30 --- /dev/null +++ b/part_5/tic_tac_toe_v4/storage/json_game_storage.go @@ -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 +} diff --git a/part_6/6.1/golang/todo/.vscode/launch.json b/part_6/6.1/golang/todo/.vscode/launch.json new file mode 100644 index 0000000..87f8444 --- /dev/null +++ b/part_6/6.1/golang/todo/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Go App", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${workspaceFolder}", + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/part_6/6.1/golang/todo/db/db.go b/part_6/6.1/golang/todo/db/db.go new file mode 100644 index 0000000..74d0e46 --- /dev/null +++ b/part_6/6.1/golang/todo/db/db.go @@ -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() +} diff --git a/part_6/6.1/golang/todo/db/db_definition.go b/part_6/6.1/golang/todo/db/db_definition.go new file mode 100644 index 0000000..eb20ce2 --- /dev/null +++ b/part_6/6.1/golang/todo/db/db_definition.go @@ -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) +); +` diff --git a/part_6/6.1/golang/todo/db/models.go b/part_6/6.1/golang/todo/db/models.go new file mode 100644 index 0000000..13bf319 --- /dev/null +++ b/part_6/6.1/golang/todo/db/models.go @@ -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 +} diff --git a/part_6/6.1/golang/todo/db/projects_crud.go b/part_6/6.1/golang/todo/db/projects_crud.go new file mode 100644 index 0000000..64110ad --- /dev/null +++ b/part_6/6.1/golang/todo/db/projects_crud.go @@ -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 +} diff --git a/part_6/6.1/golang/todo/db/tasks_crud.go b/part_6/6.1/golang/todo/db/tasks_crud.go new file mode 100644 index 0000000..a9171ca --- /dev/null +++ b/part_6/6.1/golang/todo/db/tasks_crud.go @@ -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 +} diff --git a/part_6/6.1/golang/todo/go.mod b/part_6/6.1/golang/todo/go.mod new file mode 100644 index 0000000..3843df9 --- /dev/null +++ b/part_6/6.1/golang/todo/go.mod @@ -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 +) diff --git a/part_6/6.1/golang/todo/go.sum b/part_6/6.1/golang/todo/go.sum new file mode 100644 index 0000000..80085bc --- /dev/null +++ b/part_6/6.1/golang/todo/go.sum @@ -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= diff --git a/part_6/6.1/golang/todo/main.go b/part_6/6.1/golang/todo/main.go new file mode 100644 index 0000000..6194dec --- /dev/null +++ b/part_6/6.1/golang/todo/main.go @@ -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) + } +} diff --git a/part_6/6.1/golang/todo/menu/menu.go b/part_6/6.1/golang/todo/menu/menu.go new file mode 100644 index 0000000..bf4872d --- /dev/null +++ b/part_6/6.1/golang/todo/menu/menu.go @@ -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 +} diff --git a/part_6/6.1/golang/todo/menu/project_handlers.go b/part_6/6.1/golang/todo/menu/project_handlers.go new file mode 100644 index 0000000..85dbd20 --- /dev/null +++ b/part_6/6.1/golang/todo/menu/project_handlers.go @@ -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) + } + } +} diff --git a/part_6/6.1/golang/todo/menu/task_handlers.go b/part_6/6.1/golang/todo/menu/task_handlers.go new file mode 100644 index 0000000..5fc2747 --- /dev/null +++ b/part_6/6.1/golang/todo/menu/task_handlers.go @@ -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!") +} diff --git a/part_6/6.1/golang/todo/todo.db b/part_6/6.1/golang/todo/todo.db new file mode 100644 index 0000000000000000000000000000000000000000..efe66a63a70f7bdf90ea2ef4633c9f52cabbfa7a GIT binary patch literal 20480 zcmeI)&u`mQ902fVC#jQYt(kU+I0U{(pe0(@wL%jT2#GYcvd}Ea(iBl4h+O+Ed63wd z?a=MElp!QQ{0V^g3#viYXh0`UT%Y$Ja2i75!U=KWYg1S4&`xk*;`=1q@5lE&KY!#) z?D%b5Ylwiub=Mo20ldhbVv53E17M8lxMgrll8TvhQoy5fBLBFo&X(S~Jg@)G((@0P z{<8k#!q1qYL;@s00wh2JBtQZrKmsH{0wnM+2<)CqYlT8VxswUZ?I90L|2;q6WvU&c z(lwx4S#1~)SC#TPVMD#yHEKo&+MW8Fr|xMOi2j>7zsu*>;!J2h9ha6#FMP5c`} z{L$O<`He{&d%BoD<@88wDZ2he>{*`X6|GP#D${;EW^%gZsGb@nsr^Ubo6Y*QO#=lU z<<6HcsdI&DQDMTd`OTv#o0Gte%f006y_F=F!W%6c-E8x?srnC0{|R@LNPq-LfCNZ@ z1W14cNPq-LfCNZ@1pb=>&!}luTxsD$lr3(0-RNmg^fn)Y0f$>yQ&iII>?<|5RN!HTSRg{|}k|oBr_0zd*VI5+DH*AOR8}0TLhq5+DH*AORBiUkl{aR6&V9`N*D8 z@F_s@9e_HohfG^oXZlzASbtrAZsC`OkFiLJ1W14cNPq-LfCNZ@1W14cNPq+$C-7=k zV=wfSY)*bD_vJzOnfwlqpNG5QZMi28!%xCpkTVEB2&eMC+z&sLv(!79 z#@_BLSw(&!_rg2!Fq}p)7=0Uuxr_Y{aKJJ|*PGjfSar}nsNB@1_;i5kXjGNrEgwMfrY~Z*A-jMq~SOeE` zhi1T4^?XKS=c?GPJy>fGme!U);2Hk(+j(Tha-oy%y!zVatKdy>q)M~ZCa%~=c*ng>5$7#G_dLmi?;Zr%6cs6G;38)u5spFvJtD&R;O`ip0yF5KnyV)oHSc$4LynC)v5*x%*u5U^}_? z^%_@<-W0*mQS?pnBGEMR9;Jkk3vlG%NP`ZMY?^>qZ7Tjx(F(U8CFrWYmpa2S>01BW03ZMWApum4h;Lb-`W2I2g?h57X4cI6UpE6fh}5DoOi}$*AqVU=Z8ZUaCfT9bfDAYn=~vp z>g`UeRE3e*H*a;*y2WCp%! z^YPbh5f0t_g_ys=2?GkC01BW03ZMWApa2S>01BW03ZTHhQ{WXnOY)lypV3|B1fBTq zQv5WZsy?H)At$e8$?`iD(Grd~bVf z01BW03ZMWApa2Ry{{j~?c`dzDpyx8n3l;JB`Ts*=J~AIZ{}aR+pa2S>01BW03ZMWA zpa2S>01BYM|59LC&lI$oKVKLZHMk3q{trNpum6puuZTG|zcEMV2j*)_ze5rO3ZMWA zpa2S>01BW03ZMWApa2RyBY_)+K|bke^IG(EbUeNrJs2O+=#)l3kH3gcqI=PU==xbNzn(Qn{_TbNg)K)pR~*US7YFnJ_EmtsMs5xHl literal 0 HcmV?d00001 diff --git a/part_6/6.3/golang/todo_transaction/go.mod b/part_6/6.3/golang/todo_transaction/go.mod new file mode 100644 index 0000000..123b5b0 --- /dev/null +++ b/part_6/6.3/golang/todo_transaction/go.mod @@ -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 +) diff --git a/part_6/6.3/golang/todo_transaction/go.sum b/part_6/6.3/golang/todo_transaction/go.sum new file mode 100644 index 0000000..0b0dab8 --- /dev/null +++ b/part_6/6.3/golang/todo_transaction/go.sum @@ -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= diff --git a/part_6/6.3/golang/todo_transaction/main.go b/part_6/6.3/golang/todo_transaction/main.go new file mode 100644 index 0000000..5c19a1d --- /dev/null +++ b/part_6/6.3/golang/todo_transaction/main.go @@ -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 +} diff --git a/part_6/6.3/golang/todo_transaction/todo.db b/part_6/6.3/golang/todo_transaction/todo.db new file mode 100644 index 0000000000000000000000000000000000000000..c9f63bde6aea665059e97d12219efab92b120f94 GIT binary patch literal 16384 zcmeI3L2nyH6vt0gRtX#`BwS}}OpTq`4k$vzyID`|rS^{5 zU6b^dG(`x6RP_^}_7g-cQKPg?)f4J%_QsVSk+>tc@YcjpN-QL#a6xaRS?|oedGmii z?Pcb9YRe_tXY?v}yQWVskaI+m$orHMLSj%OP=`f^my>O2IPed@F*5h*LPGnED2eAp zdq?{bey|_`B!C2v01`j~NB{{S0VIF~kidUJ;Ks6|PR-0nH%;GM@35ZBKWCQT@J;WU z_d+9*uj;wFPV2c#C7l{Cj2ZO2VK)tGJ3ebMmzFDaT3#uYW(~*eGK2bT(?8MKWS-^P zJ>TZesj;4GbJzB-k8);vjV5=%pVRL$*S1c$9LgD~VyRmEC|6yjSM=4tgr;Znm2$0K z%@xaaYFxe67)~FAJ!~2Da;2&l3+1DULnBxtO{@B4y{ebMUfmbX@00|%gB!C2v01`j~NB{{S0VIF~kiZ!e zP@@qtnOJMA#Uc?hk*M%FB|;vux}i$n1^xFz<)BPyN*J1}rK zJw5%Z93iP}#bI=nnQlEi)d{ot)NeC-9ePp{_`O)*Rc)yEQORqd0=C{R@B6mHO2cHl#T~2fy3Fw@{7t{*n%yqwjAmq&yb{i+`EK9x wkInla+8cC*iYk>QYVjt6Z9m8;f{c$%*A6dy98V~4y56@t%?y}-c{J$#380?w>i_@% literal 0 HcmV?d00001 diff --git a/part_6/tic_tac_toe/.vscode/launch.json b/part_6/tic_tac_toe/.vscode/launch.json new file mode 100644 index 0000000..edd87ae --- /dev/null +++ b/part_6/tic_tac_toe/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + "version": "0.2.0", + "configurations": [ + + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}", // <- ставим запятую + "console": "integratedTerminal" + } + ] +} diff --git a/part_6/tic_tac_toe/game/board.go b/part_6/tic_tac_toe/game/board.go new file mode 100644 index 0000000..ff0290a --- /dev/null +++ b/part_6/tic_tac_toe/game/board.go @@ -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 +} diff --git a/part_6/tic_tac_toe/game/board_cell_type.go b/part_6/tic_tac_toe/game/board_cell_type.go new file mode 100644 index 0000000..6763716 --- /dev/null +++ b/part_6/tic_tac_toe/game/board_cell_type.go @@ -0,0 +1,10 @@ +package game + +type BoardField int + +// фигуры в клетке поля +const ( + empty BoardField = iota + cross + nought +) diff --git a/part_6/tic_tac_toe/game/game.go b/part_6/tic_tac_toe/game/game.go new file mode 100644 index 0000000..4934b45 --- /dev/null +++ b/part_6/tic_tac_toe/game/game.go @@ -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!") + } +} diff --git a/part_6/tic_tac_toe/game/game_state.go b/part_6/tic_tac_toe/game/game_state.go new file mode 100644 index 0000000..2539e8f --- /dev/null +++ b/part_6/tic_tac_toe/game/game_state.go @@ -0,0 +1,12 @@ +package game + +type GameState int + +// состояние игрового процесса +const ( + playing GameState = iota + draw + crossWin + noughtWin + quit +) diff --git a/part_5/tic_tac_toe/game/i_game_loader.go b/part_6/tic_tac_toe/game/i_game_loader.go similarity index 100% rename from part_5/tic_tac_toe/game/i_game_loader.go rename to part_6/tic_tac_toe/game/i_game_loader.go diff --git a/part_5/tic_tac_toe/game/json_game_loader.go b/part_6/tic_tac_toe/game/json_game_loader.go similarity index 100% rename from part_5/tic_tac_toe/game/json_game_loader.go rename to part_6/tic_tac_toe/game/json_game_loader.go diff --git a/part_6/tic_tac_toe/game/player.go b/part_6/tic_tac_toe/game/player.go new file mode 100644 index 0000000..2441444 --- /dev/null +++ b/part_6/tic_tac_toe/game/player.go @@ -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" +} diff --git a/part_6/tic_tac_toe/go.mod b/part_6/tic_tac_toe/go.mod new file mode 100644 index 0000000..ced943a --- /dev/null +++ b/part_6/tic_tac_toe/go.mod @@ -0,0 +1,3 @@ +module tic-tac-toe + +go 1.24.0 diff --git a/part_5/tic_tac_toe/main.go b/part_6/tic_tac_toe/main.go similarity index 100% rename from part_5/tic_tac_toe/main.go rename to part_6/tic_tac_toe/main.go diff --git a/part_6/tic_tac_toe/myGame.json b/part_6/tic_tac_toe/myGame.json new file mode 100644 index 0000000..8b2b247 --- /dev/null +++ b/part_6/tic_tac_toe/myGame.json @@ -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}